From c2c70366cc267825692180ba9fa366339d4b8fa7 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 2 May 2022 22:58:31 -0400 Subject: [PATCH] no access to deployments / updates outside of users scope --- core/src/routes/deployments.ts | 27 +++- core/src/routes/updates.ts | 114 +++++++++++++-- frontend/src/components/builds/Build.tsx | 13 +- frontend/src/components/builds/Header.tsx | 19 ++- .../src/components/deployment/Deployment.tsx | 31 ++++- frontend/src/components/deployment/Header.tsx | 21 ++- .../src/components/deployment/Updates.tsx | 13 +- .../src/components/deployment/tabs/Tabs.tsx | 130 +++++++++--------- frontend/src/components/server/Header.tsx | 17 ++- frontend/src/components/server/Server.tsx | 13 +- frontend/src/state/socket.ts | 20 ++- 11 files changed, 311 insertions(+), 107 deletions(-) diff --git a/core/src/routes/deployments.ts b/core/src/routes/deployments.ts index b95adc166..3ea8d225e 100644 --- a/core/src/routes/deployments.ts +++ b/core/src/routes/deployments.ts @@ -66,6 +66,18 @@ const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => { res.send("could not find deployment"); return; } + const user = await app.users.findById( + req.user.id, + "username permissions" + ); + if ( + !user || + (user?.permissions! < 2 && !deployment.owners.includes(user.username)) + ) { + res.status(403); + res.send("user does not have permissions to view this information"); + return; + } const onCore = deployment.serverID === app.core._id; const server = onCore ? app.core @@ -90,13 +102,25 @@ const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => { const { tail } = req.query as { tail?: number }; const deployment = await app.deployments.findById( id, - "serverID containerName" + "serverID containerName owners" ); if (!deployment) { res.status(400); res.send("could not find deployment"); return; } + const user = await app.users.findById( + req.user.id, + "username permissions" + ); + if ( + !user || + (user?.permissions! < 2 && !deployment.owners.includes(user.username)) + ) { + res.status(403); + res.send("user does not have permissions to view this log"); + return; + } const onCore = deployment.serverID === app.core._id; const server = onCore ? app.core @@ -141,6 +165,7 @@ const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => { ) { res.status(403); res.send("user not authorized for this action"); + return; } const server = await app.servers.findById(deployment.serverID!); if (!server) { diff --git a/core/src/routes/updates.ts b/core/src/routes/updates.ts index 15fb4af5f..dd4a7df2c 100644 --- a/core/src/routes/updates.ts +++ b/core/src/routes/updates.ts @@ -1,5 +1,6 @@ import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; +import removeServer from "../messages/servers/remove"; const updates = fp((app: FastifyInstance, _: {}, done: () => void) => { app.get( @@ -13,18 +14,109 @@ const updates = fp((app: FastifyInstance, _: {}, done: () => void) => { serverID?: string; deploymentID?: string; }; - const updates = await app.updates.getMostRecent( - 10, - buildID - ? { buildID } - : deploymentID - ? { deploymentID } - : serverID - ? { serverID } - : {}, - offset + const user = await app.users.findById( + req.user.id, + "username permissions" ); - res.send(updates); + if (!user) { + res.status(400); + res.send("user not found"); + return; + } + if (user.permissions! < 1) { + res.status(403); + res.send("user does not have permission to access this information"); + return; + } + if (user.permissions! > 1) { + const updates = await app.updates.getMostRecent( + 10, + buildID + ? { buildID } + : deploymentID + ? { deploymentID } + : serverID + ? { serverID } + : {}, + offset + ); + res.send(updates); + return; + } + if (buildID) { + const build = await app.builds.findById(buildID, "owners"); + if (!build || !build.owners.includes(user.username)) { + res.status(403); + res.send("user does not have permission to access this data"); + return; + } + const updates = await app.updates.getMostRecent( + 10, + { buildID }, + offset + ); + res.send(updates); + } else if (deploymentID) { + const deployment = await app.deployments.findById( + deploymentID, + "owners" + ); + if (!deployment || !deployment.owners.includes(user.username)) { + res.status(403); + res.send("user does not have permission to access this data"); + return; + } + const updates = await app.updates.getMostRecent( + 10, + { deploymentID }, + offset + ); + res.send(updates); + } else if (serverID) { + const server = await app.servers.findById(serverID, "owners"); + if (!server || !server.owners.includes(user.username)) { + res.status(403); + res.send("user does not have permission to access this data"); + return; + } + const updates = await app.updates.getMostRecent( + 10, + { serverID }, + offset + ); + res.send(updates); + } else { + const deploymentIDs = ( + await app.deployments.find( + { owners: { $elemMatch: { $eq: user.username } } }, + "_id" + ) + ).map((dep) => dep._id); + const buildIDs = ( + await app.builds.find( + { owners: { $elemMatch: { $eq: user.username } } }, + "_id" + ) + ).map((build) => build._id); + const serverIDs = ( + await app.servers.find( + { owners: { $elemMatch: { $eq: user.username } } }, + "_id" + ) + ).map((server) => server._id); + const updates = await app.updates.getMostRecent( + 10, + { + $or: [ + { deploymentID: { $in: deploymentIDs } }, + { buildID: { $in: buildIDs } }, + { serverID: { $in: serverIDs } }, + ], + }, + offset + ); + res.send(updates); + } } ); done(); diff --git a/frontend/src/components/builds/Build.tsx b/frontend/src/components/builds/Build.tsx index e455a457e..4ce0b7175 100644 --- a/frontend/src/components/builds/Build.tsx +++ b/frontend/src/components/builds/Build.tsx @@ -2,6 +2,7 @@ import { Component, Show } from "solid-js"; import { useAppDimensions } from "../../state/DimensionProvider"; import { useAppState } from "../../state/StateProvider"; import { useTheme } from "../../state/ThemeProvider"; +import { useUser } from "../../state/UserProvider"; import { combineClasses } from "../../util/helpers"; import NotFound from "../NotFound"; import Grid from "../util/layout/Grid"; @@ -16,6 +17,16 @@ const Build: Component<{}> = (p) => { const build = () => builds.get(selected.id())!; const { themeClass } = useTheme(); const { isMobile } = useAppDimensions(); + const { permissions, username } = useUser(); + const userCanUpdate = () => { + if (permissions() > 1) { + return true; + } else if (permissions() > 0 && build()!.owners.includes(username()!)) { + return true; + } else { + return false; + } + }; return ( }> @@ -24,7 +35,7 @@ const Build: Component<{}> = (p) => {
- + diff --git a/frontend/src/components/builds/Header.tsx b/frontend/src/components/builds/Header.tsx index b4202da1d..0de07f7ee 100644 --- a/frontend/src/components/builds/Header.tsx +++ b/frontend/src/components/builds/Header.tsx @@ -23,6 +23,15 @@ const Header: Component<{}> = (p) => { const { isMobile } = useAppDimensions(); const [showUpdates, toggleShowUpdates] = useLocalStorageToggle("show-updates"); + const userCanUpdate = () => { + if (permissions() > 1) { + return true; + } else if (permissions() > 0 && build()!.owners.includes(username()!)) { + return true; + } else { + return false; + } + }; return ( <> = (p) => { alignItems="center" style={{ position: "relative", - cursor: isMobile() ? "pointer" : undefined, + cursor: isMobile() && userCanUpdate() ? "pointer" : undefined, }} onClick={() => { - if (isMobile()) toggleShowUpdates(); + if (isMobile() && userCanUpdate()) toggleShowUpdates(); }} >

{build().name}

{getSub(build())}
- = 2 || build().owners.includes(username()!)}> + = (p) => { - + updates{" "} = (p) => {
- + diff --git a/frontend/src/components/deployment/Deployment.tsx b/frontend/src/components/deployment/Deployment.tsx index 132a4c1bf..2c2ae9a37 100644 --- a/frontend/src/components/deployment/Deployment.tsx +++ b/frontend/src/components/deployment/Deployment.tsx @@ -2,13 +2,14 @@ import { Component, Show } from "solid-js"; import { useAppDimensions } from "../../state/DimensionProvider"; import { useAppState } from "../../state/StateProvider"; import { useTheme } from "../../state/ThemeProvider"; +import { useUser } from "../../state/UserProvider"; import { combineClasses } from "../../util/helpers"; import NotFound from "../NotFound"; import Grid from "../util/layout/Grid"; -import Loading from "../util/loading/Loading"; import Actions from "./Actions"; import { ActionStateProvider } from "./ActionStateProvider"; import Header from "./Header"; +import { ConfigProvider } from "./tabs/config/Provider"; import DeploymentTabs from "./tabs/Tabs"; import Updates from "./Updates"; @@ -18,6 +19,19 @@ const Deployment: Component<{}> = (p) => { const server = () => deployment() && servers.get(deployment()?.serverID!); const { themeClass } = useTheme(); const { isMobile } = useAppDimensions(); + const { permissions, username } = useUser(); + const userCanUpdate = () => { + if (permissions() > 1) { + return true; + } else if ( + permissions() > 0 && + deployment()!.owners.includes(username()!) + ) { + return true; + } else { + return false; + } + }; return ( = (p) => {
- + {/* right / tabs */} - + + you do not have permission to view this deployment + + } + > + + + + diff --git a/frontend/src/components/deployment/Header.tsx b/frontend/src/components/deployment/Header.tsx index 131f49239..7213cb91c 100644 --- a/frontend/src/components/deployment/Header.tsx +++ b/frontend/src/components/deployment/Header.tsx @@ -39,6 +39,19 @@ const Header: Component<{ exiting?: boolean }> = (p) => { const { isMobile } = useAppDimensions(); const [showUpdates, toggleShowUpdates] = useLocalStorageToggle("show-updates"); + + const userCanUpdate = () => { + if (permissions() > 1) { + return true; + } else if ( + permissions() > 0 && + deployment()!.owners.includes(username()!) + ) { + return true; + } else { + return false; + } + }; return ( <> = (p) => { class={combineClasses("card shadow", themeClass())} style={{ position: "relative", - cursor: isMobile() ? "pointer" : undefined, + cursor: (isMobile() && userCanUpdate()) ? "pointer" : undefined, }} onClick={() => { - if (isMobile()) toggleShowUpdates(); + if (isMobile() && userCanUpdate()) toggleShowUpdates(); }} > @@ -104,7 +117,7 @@ const Header: Component<{ exiting?: boolean }> = (p) => {
{status()}
- + updates{" "} = (p) => {
- + diff --git a/frontend/src/components/deployment/Updates.tsx b/frontend/src/components/deployment/Updates.tsx index 83c92064f..05980a8a1 100644 --- a/frontend/src/components/deployment/Updates.tsx +++ b/frontend/src/components/deployment/Updates.tsx @@ -14,12 +14,13 @@ const Updates: Component<{}> = (p) => { const selectedUpdates = useArray(() => getUpdates({ deploymentID: selected.id() }) ); - const unsub = ws.subscribe([ADD_UPDATE], ({ update }) => { - if (update.deploymentID === selected.id()) { - selectedUpdates.add(update); - } - }); - onCleanup(unsub); + onCleanup( + ws.subscribe([ADD_UPDATE], ({ update }) => { + if (update.deploymentID === selected.id()) { + selectedUpdates.add(update); + } + }) + ); const [noMoreUpdates, setNoMore] = createSignal(false); const loadMore = async () => { const offset = selectedUpdates.collection()?.length; diff --git a/frontend/src/components/deployment/tabs/Tabs.tsx b/frontend/src/components/deployment/tabs/Tabs.tsx index c1393ff5c..13d04cab3 100644 --- a/frontend/src/components/deployment/tabs/Tabs.tsx +++ b/frontend/src/components/deployment/tabs/Tabs.tsx @@ -9,7 +9,6 @@ import { import Tabs from "../../util/tabs/Tabs"; import Config from "./config/Config"; import Log from "./log/Log"; -import { ConfigProvider } from "./config/Provider"; import { useAppState } from "../../../state/StateProvider"; import { getDeploymentLog } from "../../../util/query"; import Icon from "../../util/Icon"; @@ -33,7 +32,7 @@ const DeploymentTabs: Component<{}> = () => { deployment()!.status === "not deployed" ? "not deployed" : (deployment()!.status as ContainerStatus).State; - const load = async () => { + const loadLog = async () => { console.log("load log"); if (deployment()?.status !== "not deployed") { const log = await getDeploymentLog(selected.id(), logTail()); @@ -42,75 +41,74 @@ const DeploymentTabs: Component<{}> = () => { setLog({}); } }; - createEffect(load); - const unsub = ws.subscribe([ADD_UPDATE], ({ update }: { update: Update }) => { - if ( - update.deploymentID === selected.id() && - (update.operation === DEPLOY || - update.operation === START_CONTAINER || - update.operation === STOP_CONTAINER || - update.operation === DELETE_CONTAINER) - ) { - // console.log("updating log"); - setTimeout(() => { - getDeploymentLog(selected.id()).then(setLog); - }, 2000); - } - }); - onCleanup(unsub); + createEffect(loadLog); + onCleanup( + ws.subscribe([ADD_UPDATE], ({ update }: { update: Update }) => { + if ( + update.deploymentID === selected.id() && + (update.operation === DEPLOY || + update.operation === START_CONTAINER || + update.operation === STOP_CONTAINER || + update.operation === DELETE_CONTAINER) + ) { + // console.log("updating log"); + setTimeout(() => { + getDeploymentLog(selected.id()).then(setLog); + }, 2000); + } + }) + ); const { themeClass } = useTheme(); return ( - - , + }, + status() !== "not deployed" && [ { - title: "config", - element: , + title: "log", + element: ( + + ), }, - status() !== "not deployed" && [ - { - title: "log", - element: ( - - ), - }, - { - title: "error log", - titleElement: ( - - error log{" "} - - - - - ), - element: ( - - ), - }, - ], - ].flat()} - localStorageKey="deployment-tab" - /> - + status() !== "not deployed" && { + title: "error log", + titleElement: ( + + error log{" "} + + + + + ), + element: ( + + ), + }, + ], + ].flat()} + localStorageKey="deployment-tab" + /> ); }; diff --git a/frontend/src/components/server/Header.tsx b/frontend/src/components/server/Header.tsx index 66bb7b32c..5a4c8bfb5 100644 --- a/frontend/src/components/server/Header.tsx +++ b/frontend/src/components/server/Header.tsx @@ -27,6 +27,15 @@ const Header: Component<{}> = (p) => { const { isMobile } = useAppDimensions(); const [showUpdates, toggleShowUpdates] = useLocalStorageToggle("show-updates"); + const userCanUpdate = () => { + if (permissions() > 1) { + return true; + } else if (permissions() > 0 && server()!.owners.includes(username()!)) { + return true; + } else { + return false; + } + }; return ( <> = (p) => { alignItems="center" style={{ position: "relative", - cursor: isMobile() ? "pointer" : undefined, + cursor: isMobile() && userCanUpdate() ? "pointer" : undefined, }} onClick={() => { - if (isMobile()) toggleShowUpdates(); + if (isMobile() && userCanUpdate()) toggleShowUpdates(); }} > @@ -64,7 +73,7 @@ const Header: Component<{}> = (p) => { - + updates{" "} = (p) => { - + diff --git a/frontend/src/components/server/Server.tsx b/frontend/src/components/server/Server.tsx index 4495a792f..632fd4083 100644 --- a/frontend/src/components/server/Server.tsx +++ b/frontend/src/components/server/Server.tsx @@ -2,6 +2,7 @@ import { Component, Show } from "solid-js"; import { useAppDimensions } from "../../state/DimensionProvider"; import { useAppState } from "../../state/StateProvider"; import { useTheme } from "../../state/ThemeProvider"; +import { useUser } from "../../state/UserProvider"; import { combineClasses } from "../../util/helpers"; import NotFound from "../NotFound"; import Grid from "../util/layout/Grid"; @@ -16,6 +17,16 @@ const Server: Component<{}> = (p) => { const server = () => servers.get(selected.id())!; const { themeClass } = useTheme(); const { isMobile } = useAppDimensions(); + const { permissions, username } = useUser(); + const userCanUpdate = () => { + if (permissions() > 1) { + return true; + } else if (permissions() > 0 && server()!.owners.includes(username()!)) { + return true; + } else { + return false; + } + }; return ( }> @@ -24,7 +35,7 @@ const Server: Component<{}> = (p) => {
- + diff --git a/frontend/src/state/socket.ts b/frontend/src/state/socket.ts index 284100679..cf92bc22f 100644 --- a/frontend/src/state/socket.ts +++ b/frontend/src/state/socket.ts @@ -23,16 +23,13 @@ import { State } from "./StateProvider"; import { createSignal } from "solid-js"; import ReconnectingWebSocket from "reconnecting-websocket"; -function socket( - user: User, - state: State, -) { +function socket(user: User, state: State) { const ws = new ReconnectingWebSocket(WS_URL); const [isOpen, setOpen] = createSignal(false); ws.addEventListener("open", () => { - console.log("connection opened") + console.log("connection opened"); ws.send(JSON.stringify({ token: client.token })); setOpen(true); }); @@ -154,6 +151,19 @@ function handleMessage( /* Updates */ case ADD_UPDATE: const { update } = message as { update: Update }; + if ( + (update.deploymentID && + !deployments + .get(update.deploymentID) + ?.owners.includes(user.username)) || + (update.buildID && + !builds.get(update.buildID)?.owners.includes(user.username)) || + (update.serverID && + !servers.get(update.serverID)?.owners.includes(user.username)) + ) { + // dont respond to updates outside of users scope. not airtight for protecting sensitive data. + return; + } updates.add(update); pushNotification( update.isError ? "bad" : "good",