no access to deployments / updates outside of users scope

This commit is contained in:
mbecker20
2022-05-02 22:58:31 -04:00
parent 6752a539aa
commit c2c70366cc
11 changed files with 311 additions and 107 deletions
+26 -1
View File
@@ -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) {
+103 -11
View File
@@ -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();
+12 -1
View File
@@ -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 (
<Show when={build()} fallback={<NotFound type="build" />}>
<ActionStateProvider>
@@ -24,7 +35,7 @@ const Build: Component<{}> = (p) => {
<Grid class="left-content">
<Header />
<Actions />
<Show when={!isMobile()}>
<Show when={!isMobile() && userCanUpdate()}>
<Updates />
</Show>
</Grid>
+14 -5
View File
@@ -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 (
<>
<Flex
@@ -31,17 +40,17 @@ const Header: Component<{}> = (p) => {
alignItems="center"
style={{
position: "relative",
cursor: isMobile() ? "pointer" : undefined,
cursor: isMobile() && userCanUpdate() ? "pointer" : undefined,
}}
onClick={() => {
if (isMobile()) toggleShowUpdates();
if (isMobile() && userCanUpdate()) toggleShowUpdates();
}}
>
<Grid gap="0.1rem">
<h1>{build().name}</h1>
<div style={{ opacity: 0.8 }}>{getSub(build())}</div>
</Grid>
<Show when={permissions() >= 2 || build().owners.includes(username()!)}>
<Show when={userCanUpdate()}>
<Show
when={!actions.deleting}
fallback={
@@ -60,7 +69,7 @@ const Header: Component<{}> = (p) => {
</ConfirmButton>
</Show>
</Show>
<Show when={isMobile()}>
<Show when={isMobile() && userCanUpdate()}>
<Flex gap="0.5rem" alignItems="center" class="show-updates-indicator">
updates{" "}
<Icon
@@ -70,7 +79,7 @@ const Header: Component<{}> = (p) => {
</Flex>
</Show>
</Flex>
<Show when={isMobile() && showUpdates()}>
<Show when={isMobile() && userCanUpdate() && showUpdates()}>
<Updates />
</Show>
</>
@@ -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 (
<Show
when={deployment() && server()}
@@ -29,12 +43,23 @@ const Deployment: Component<{}> = (p) => {
<Grid class="left-content">
<Header />
<Actions />
<Show when={!isMobile()}>
<Show when={!isMobile() && userCanUpdate()}>
<Updates />
</Show>
</Grid>
{/* right / tabs */}
<DeploymentTabs />
<Show
when={userCanUpdate()}
fallback={
<h2 class={combineClasses("card tabs shadow", themeClass())}>
you do not have permission to view this deployment
</h2>
}
>
<ConfigProvider>
<DeploymentTabs />
</ConfigProvider>
</Show>
</Grid>
</ActionStateProvider>
</Show>
+17 -4
View File
@@ -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 (
<>
<Grid
@@ -46,10 +59,10 @@ const Header: Component<{ exiting?: boolean }> = (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();
}}
>
<Flex alignItems="center" justifyContent="space-between">
@@ -104,7 +117,7 @@ const Header: Component<{ exiting?: boolean }> = (p) => {
<div style={{ opacity: 0.7 }}>{status()}</div>
</Show>
</Flex>
<Show when={isMobile()}>
<Show when={isMobile() && userCanUpdate()}>
<Flex gap="0.5rem" alignItems="center" class="show-updates-indicator">
updates{" "}
<Icon
@@ -114,7 +127,7 @@ const Header: Component<{ exiting?: boolean }> = (p) => {
</Flex>
</Show>
</Grid>
<Show when={isMobile() && showUpdates()}>
<Show when={isMobile() && userCanUpdate() && showUpdates()}>
<Updates />
</Show>
</>
@@ -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;
@@ -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 (
<Show when={deployment()}>
<ConfigProvider>
<Tabs
containerClass={combineClasses("card tabs shadow", themeClass())}
containerStyle={{ gap: "0.5rem" }}
tabs={[
<Tabs
containerClass={combineClasses("card tabs shadow", themeClass())}
containerStyle={{ gap: "0.5rem" }}
tabs={[
{
title: "config",
element: <Config />,
},
status() !== "not deployed" && [
{
title: "config",
element: <Config />,
title: "log",
element: (
<Log
reload={loadLog}
log={log()}
logTail={logTail()}
setLogTail={setLogTail}
/>
),
},
status() !== "not deployed" && [
{
title: "log",
element: (
<Log
reload={load}
log={log()}
logTail={logTail()}
setLogTail={setLogTail}
/>
),
},
{
title: "error log",
titleElement: (
<Flex gap="0.5rem" alignItems="center">
error log{" "}
<Show
when={
deployment()!.status !== "not deployed" && log().stderr
}
>
<Icon type="error" />
</Show>
</Flex>
),
element: (
<Log
reload={load}
log={log()}
logTail={logTail()}
setLogTail={setLogTail}
error
/>
),
},
],
].flat()}
localStorageKey="deployment-tab"
/>
</ConfigProvider>
status() !== "not deployed" && {
title: "error log",
titleElement: (
<Flex gap="0.5rem" alignItems="center">
error log{" "}
<Show
when={
deployment()!.status !== "not deployed" && log().stderr
}
>
<Icon type="error" />
</Show>
</Flex>
),
element: (
<Log
reload={loadLog}
log={log()}
logTail={logTail()}
setLogTail={setLogTail}
error
/>
),
},
],
].flat()}
localStorageKey="deployment-tab"
/>
</Show>
);
};
+13 -4
View File
@@ -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 (
<>
<Flex
@@ -35,10 +44,10 @@ const Header: Component<{}> = (p) => {
alignItems="center"
style={{
position: "relative",
cursor: isMobile() ? "pointer" : undefined,
cursor: isMobile() && userCanUpdate() ? "pointer" : undefined,
}}
onClick={() => {
if (isMobile()) toggleShowUpdates();
if (isMobile() && userCanUpdate()) toggleShowUpdates();
}}
>
<Grid gap="0.1rem">
@@ -64,7 +73,7 @@ const Header: Component<{}> = (p) => {
</Show>
</Flex>
</Show>
<Show when={isMobile()}>
<Show when={isMobile() && userCanUpdate()}>
<Flex gap="0.5rem" alignItems="center" class="show-updates-indicator">
updates{" "}
<Icon
@@ -74,7 +83,7 @@ const Header: Component<{}> = (p) => {
</Flex>
</Show>
</Flex>
<Show when={isMobile() && showUpdates()}>
<Show when={isMobile() && userCanUpdate() && showUpdates()}>
<Updates />
</Show>
</>
+12 -1
View File
@@ -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 (
<Show when={server()} fallback={<NotFound type="server" />}>
<ActionStateProvider>
@@ -24,7 +35,7 @@ const Server: Component<{}> = (p) => {
<Grid class="left-content">
<Header />
<Actions />
<Show when={!isMobile()}>
<Show when={!isMobile() && userCanUpdate()}>
<Updates />
</Show>
</Grid>
+15 -5
View File
@@ -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",