From cdea22c08a7a964884df034cacbb5c394b40d52c Mon Sep 17 00:00:00 2001 From: Dunemask Date: Sun, 4 Feb 2024 17:02:15 -0700 Subject: [PATCH] [FEATURE] Cairo Auth Integration --- lib/controllers/file-controller.js | 11 +++++++++ lib/controllers/lifecycle-controller.js | 18 ++++++++------ lib/controllers/status-controller.js | 6 ++--- .../migrations/1_create_servers_table.sql | 1 + lib/database/queries/server-queries.js | 23 ++++++++++++++---- lib/k8s/k8s-server-control.js | 15 ++++++++++++ lib/k8s/server-status.js | 6 ++--- lib/routes/files-route.js | 4 +++- lib/routes/middlewares/auth-middleware.js | 1 + lib/routes/server-route.js | 4 ++-- lib/routes/system-route.js | 4 ++++ src/components/files/FilePreview.jsx | 3 ++- src/components/files/MineclusterFiles.jsx | 2 ++ src/nav/Viewport.jsx | 3 --- src/util/auth.js | 24 ++++--------------- src/util/queries.js | 9 ++++++- 16 files changed, 89 insertions(+), 45 deletions(-) diff --git a/lib/controllers/file-controller.js b/lib/controllers/file-controller.js index 1a92c17..83e1ab5 100644 --- a/lib/controllers/file-controller.js +++ b/lib/controllers/file-controller.js @@ -6,11 +6,14 @@ import { uploadServerItem, } from "../k8s/server-files.js"; import { sendError } from "../util/ExpressClientError.js"; +import { checkAuthorization } from "../database/queries/server-queries.js"; export async function listFiles(req, res) { const serverSpec = req.body; if (!serverSpec) return res.sendStatus(400); if (!serverSpec.id) return res.status(400).send("Server id missing!"); + const authorized = await checkAuthorization(serverSpec.id, req.cairoId); + if (!authorized) return res.sendStatus(403); listServerFiles(serverSpec) .then((f) => { const fileData = f.map((fi, i) => ({ @@ -31,6 +34,8 @@ export async function createFolder(req, res) { if (!serverSpec) return res.sendStatus(400); if (!serverSpec.id) return res.status(400).send("Server id missing!"); if (!serverSpec.path) return res.status(400).send("Path required!"); + const authorized = await checkAuthorization(serverSpec.id, req.cairoId); + if (!authorized) return res.sendStatus(403); createServerFolder(serverSpec) .then(() => res.sendStatus(200)) .catch(sendError(res)); @@ -43,6 +48,8 @@ export async function deleteItem(req, res) { if (!serverSpec.path) return res.status(400).send("Path required!"); if (serverSpec.isDir === undefined || serverSpec.isDir === null) return res.status(400).send("IsDIr required!"); + const authorized = await checkAuthorization(serverSpec.id, req.cairoId); + if (!authorized) return res.sendStatus(403); removeServerItem(serverSpec) .then(() => res.sendStatus(200)) .catch(sendError(res)); @@ -52,6 +59,8 @@ export async function uploadItem(req, res) { const serverSpec = req.body; if (!serverSpec.id) return res.status(400).send("Server id missing!"); if (!serverSpec.path) return res.status(400).send("Path required!"); + const authorized = await checkAuthorization(serverSpec.id, req.cairoId); + if (!authorized) return res.sendStatus(403); uploadServerItem(serverSpec, req.file) .then(() => res.sendStatus(200)) .catch(sendError(res)); @@ -61,6 +70,8 @@ export async function getItem(req, res) { const serverSpec = req.body; if (!serverSpec.id) return res.status(400).send("Server id missing!"); if (!serverSpec.path) return res.status(400).send("Path required!"); + const authorized = await checkAuthorization(serverSpec.id, req.cairoId); + if (!authorized) return res.sendStatus(403); getServerItem(serverSpec, res) .then(({ ds, ftpTransfer }) => { ds.pipe(res).on("error", sendError(res)); diff --git a/lib/controllers/lifecycle-controller.js b/lib/controllers/lifecycle-controller.js index 47acb41..d5b9e3a 100644 --- a/lib/controllers/lifecycle-controller.js +++ b/lib/controllers/lifecycle-controller.js @@ -8,6 +8,7 @@ import { } from "../database/queries/server-queries.js"; import ExpressClientError, { sendError } from "../util/ExpressClientError.js"; import { toggleServer } from "../k8s/k8s-server-control.js"; +import { checkAuthorization } from "../database/queries/server-queries.js"; const dnsRegex = new RegExp( `^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`, @@ -71,10 +72,13 @@ function payloadFilter(req, res) { return "filtered"; } -function checkServerId(serverSpec) { +async function checkServerId(cairoId, serverSpec) { if (!serverSpec) throw new ExpressClientError({ c: 400 }); if (!serverSpec.id) throw new ExpressClientError({ c: 400, m: "Server id missing!" }); + const authorized = await checkAuthorization(serverSpec.id, cairoId); + if (!authorized) + throw new ExpressClientError({ c: 403, m: "Access forbidden!" }); } export async function createServer(req, res) { @@ -82,7 +86,7 @@ export async function createServer(req, res) { if (backupPayloadFilter(req, res) !== "filtered") return; const serverSpec = req.body; try { - const serverEntry = await createServerEntry(serverSpec); + const serverEntry = await createServerEntry(req.cairoId, serverSpec); await createServerResources(serverEntry); res.json(serverEntry); } catch (e) { @@ -94,7 +98,7 @@ export async function deleteServer(req, res) { // Ensure spec is safe const serverSpec = req.body; try { - checkServerId(serverSpec); + await checkServerId(req.cairoId, serverSpec); } catch (e) { return sendError(res)(e); } @@ -109,7 +113,7 @@ export async function startServer(req, res) { // Ensure spec is safe const serverSpec = req.body; try { - checkServerId(serverSpec); + await checkServerId(req.cairoId, serverSpec); } catch (e) { return sendError(res)(e); } @@ -123,7 +127,7 @@ export async function stopServer(req, res) { // Ensure spec is safe const serverSpec = req.body; try { - checkServerId(serverSpec); + await checkServerId(req.cairoId, serverSpec); } catch (e) { return sendError(res)(e); } @@ -137,7 +141,7 @@ export async function getServer(req, res) { // Ensure spec is safe const serverSpec = req.body; try { - checkServerId(serverSpec); + await checkServerId(req.cairoId, serverSpec); } catch (e) { return sendError(res)(e); } @@ -155,7 +159,7 @@ export async function modifyServer(req, res) { if (payloadFilter(req, res) !== "filtered") return; const serverSpec = req.body; try { - checkServerId(serverSpec); + await checkServerId(req.cairoId, serverSpec); const serverEntry = await modifyServerEntry(serverSpec); // await createServerResources(serverEntry); res.sendStatus(200); diff --git a/lib/controllers/status-controller.js b/lib/controllers/status-controller.js index cd5ff57..a36a53b 100644 --- a/lib/controllers/status-controller.js +++ b/lib/controllers/status-controller.js @@ -1,9 +1,9 @@ -import { getDeployments } from "../k8s/k8s-server-control.js"; +import { getUserDeployments } from "../k8s/k8s-server-control.js"; import { getInstances } from "../k8s/server-status.js"; import { sendError } from "../util/ExpressClientError.js"; export function serverList(req, res) { - getDeployments() + getUserDeployments(req.cairoId) .then((sd) => res.json(sd.map((s) => s.metadata.name.substring(4)))) .catch((e) => { ERR("STATUS CONTROLLER", e); @@ -12,7 +12,7 @@ export function serverList(req, res) { } export function serverInstances(req, res) { - getInstances() + getInstances(req.cairoId) .then((i) => res.json(i)) .catch(sendError(res)); } diff --git a/lib/database/migrations/1_create_servers_table.sql b/lib/database/migrations/1_create_servers_table.sql index 33ddc5a..fe3f357 100644 --- a/lib/database/migrations/1_create_servers_table.sql +++ b/lib/database/migrations/1_create_servers_table.sql @@ -1,6 +1,7 @@ CREATE SEQUENCE servers_id_seq; CREATE TABLE servers ( id bigint NOT NULL DEFAULT nextval('servers_id_seq') PRIMARY KEY, + owner_cairo_id bigint, host varchar(255) DEFAULT NULL, name varchar(255) DEFAULT NULL, version varchar(63) DEFAULT 'latest', diff --git a/lib/database/queries/server-queries.js b/lib/database/queries/server-queries.js index c98902e..326572b 100644 --- a/lib/database/queries/server-queries.js +++ b/lib/database/queries/server-queries.js @@ -2,7 +2,7 @@ import pg from "../postgres.js"; import { deleteQuery, insertQuery, - selectWhereQuery, + selectWhereAllQuery, updateWhereAllQuery, } from "../pg-query.js"; import ExpressClientError from "../../util/ExpressClientError.js"; @@ -12,9 +12,18 @@ const asExpressClientError = (e) => { throw new ExpressClientError({ m: e.message, c: 409 }); }; -const getMclName = (host, id) => `${host.replaceAll(".", "-")}-${id}`; +const getMclName = (host, id) => + `${host.toLowerCase().replaceAll(".", "-")}-${id}`; -export async function createServerEntry(serverSpec) { +export async function checkAuthorization(serverId, cairoId) { + const q = selectWhereAllQuery(table, { + id: serverId, + owner_cairo_id: cairoId, + }); + return (await pg.query(q)).length === 1; +} + +export async function createServerEntry(cairoId, serverSpec) { const { name, host, @@ -33,6 +42,7 @@ export async function createServerEntry(serverSpec) { var q = insertQuery(table, { name, + owner_cairo_id: cairoId, host, version, server_type, @@ -52,6 +62,7 @@ export async function createServerEntry(serverSpec) { const entries = await pg.query(q); const { id, + owner_cairo_id: ownerCairoId, name, host, version, @@ -72,6 +83,7 @@ export async function createServerEntry(serverSpec) { name, mclName, id, + ownerCairoId, host, version, serverType, @@ -99,7 +111,7 @@ export async function deleteServerEntry(serverId) { export async function getServerEntry(serverId) { if (!serverId) asExpressClientError({ message: "Server ID Required!" }); - const q = selectWhereQuery(table, { id: serverId }); + const q = selectWhereAllQuery(table, { id: serverId }); try { const serverSpecs = await pg.query(q); if (serverSpecs.length === 0) return []; @@ -107,6 +119,7 @@ export async function getServerEntry(serverId) { throw Error("Multiple servers found with the same name!"); const { id, + owner_cairo_id: ownerCairoId, name, host, version, @@ -127,6 +140,7 @@ export async function getServerEntry(serverId) { name, mclName, id, + ownerCairoId, host, version, serverType, @@ -149,6 +163,7 @@ export async function getServerEntry(serverId) { export async function modifyServerEntry(serverSpec) { const { id, + // ownerCairoId: owner_cairo_id, // DIsabled! If these becomes a reqest, please create a new function! name, host, version, diff --git a/lib/k8s/k8s-server-control.js b/lib/k8s/k8s-server-control.js index f82187a..ea4641e 100644 --- a/lib/k8s/k8s-server-control.js +++ b/lib/k8s/k8s-server-control.js @@ -7,6 +7,7 @@ import { getCoreServerContainer, getBackupContainer, } from "./server-containers.js"; +import { checkAuthorization } from "../database/queries/server-queries.js"; const kc = new k8s.KubeConfig(); kc.loadFromDefault(); @@ -25,6 +26,20 @@ const mineclusterManaged = (o) => export const serverMatch = (serverId) => (o) => o.metadata.annotations["minecluster.dunemask.net/id"] === serverId; +export const cairoMatch = (cairoId) => (o) => + checkAuthorization( + o.metadata.annotations["minecluster.dunemask.net/id"], + cairoId, + ); + +export async function getUserDeployments(cairoId) { + const authFIlter = cairoMatch(cairoId); + const allDeployments = await getDeployments(); + const authChecks = allDeployments.map(authFIlter); + const authorizations = await Promise.all(authChecks); + return allDeployments.filter((_d, i) => authorizations[i]); +} + export async function getDeployments() { const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); const serverDeployments = deploymentRes.body.items.filter(mineclusterManaged); diff --git a/lib/k8s/server-status.js b/lib/k8s/server-status.js index 43f00af..b1df472 100644 --- a/lib/k8s/server-status.js +++ b/lib/k8s/server-status.js @@ -1,5 +1,5 @@ import k8s from "@kubernetes/client-node"; -import { getDeployments } from "./k8s-server-control.js"; +import { getUserDeployments } from "./k8s-server-control.js"; import { getServerEntries } from "../database/queries/server-queries.js"; const kc = new k8s.KubeConfig(); kc.loadFromDefault(); @@ -42,9 +42,9 @@ function getServerStatus(server) { return { serverAvailable, ftpAvailable, services, deploymentAvailable }; } -export async function getInstances() { +export async function getInstances(cairoId) { const [serverDeployments, podMetricsRes, entries] = await Promise.all([ - getDeployments(), + getUserDeployments(cairoId), k8sMetrics.getPodMetrics(namespace), getServerEntries(), ]); diff --git a/lib/routes/files-route.js b/lib/routes/files-route.js index c27c175..1092a81 100644 --- a/lib/routes/files-route.js +++ b/lib/routes/files-route.js @@ -8,8 +8,10 @@ import { getItem, } from "../controllers/file-controller.js"; +import cairoAuthMiddleware from "./middlewares/auth-middleware.js"; + const router = Router(); -router.use(jsonMiddleware()); +router.use([jsonMiddleware(), cairoAuthMiddleware]); const multerMiddleware = multer(); router.post("/list", listFiles); diff --git a/lib/routes/middlewares/auth-middleware.js b/lib/routes/middlewares/auth-middleware.js index 29b1812..0a73234 100644 --- a/lib/routes/middlewares/auth-middleware.js +++ b/lib/routes/middlewares/auth-middleware.js @@ -19,6 +19,7 @@ const cairoAuthHandler = (req, res, next) => { if (!req.token) return res.status(401).send("Cairo auth required!"); VERB("AUTH", `${MCL_CAIRO_URL}/api/user/info`); cairoAuthenticate(req.token) + .then((authData) => (req.cairoId = authData.id)) .then(() => next()) .catch((err) => { ERR("AUTH", err.response ? err.response.data : err.message); diff --git a/lib/routes/server-route.js b/lib/routes/server-route.js index 22ead1a..d8ae832 100644 --- a/lib/routes/server-route.js +++ b/lib/routes/server-route.js @@ -15,14 +15,14 @@ import { import cairoAuthMiddleware from "./middlewares/auth-middleware.js"; const router = Router(); -router.use(jsonMiddleware()); +router.use([jsonMiddleware(), cairoAuthMiddleware]); // Routes router.post("/create", createServer); router.delete("/delete", deleteServer); router.post("/start", startServer); router.post("/stop", stopServer); router.get("/list", serverList); -router.get("/instances", cairoAuthMiddleware, serverInstances); +router.get("/instances", serverInstances); router.post("/blueprint", getServer); router.post("/modify", modifyServer); export default router; diff --git a/lib/routes/system-route.js b/lib/routes/system-route.js index 66e1022..27291a9 100644 --- a/lib/routes/system-route.js +++ b/lib/routes/system-route.js @@ -2,6 +2,10 @@ import { Router } from "express"; import k8s from "@kubernetes/client-node"; import { WARN } from "../util/logging.js"; const router = Router(); + +import cairoAuthMiddleware from "./middlewares/auth-middleware.js"; +router.use(cairoAuthMiddleware); + const kc = new k8s.KubeConfig(); kc.loadFromDefault(); const k8sApi = kc.makeApiClient(k8s.CoreV1Api); diff --git a/src/components/files/FilePreview.jsx b/src/components/files/FilePreview.jsx index 8a7d27c..0241cf5 100644 --- a/src/components/files/FilePreview.jsx +++ b/src/components/files/FilePreview.jsx @@ -7,8 +7,8 @@ import DialogContent from "@mui/material/DialogContent"; import DialogActions from "@mui/material/DialogActions"; import Dialog from "@mui/material/Dialog"; import Toolbar from "@mui/material/Toolbar"; - import TextEditor from "./TextEditor.jsx"; +import { cairoAuthHeader } from "@mcl/util/auth.js"; const textFileTypes = ["properties", "txt", "yaml", "yml", "json", "env"]; const imageFileTypes = ["png", "jpeg", "jpg"]; @@ -52,6 +52,7 @@ export default function FilePreview(props) { await fetch("/api/files/upload", { method: "POST", body: formData, + headers: cairoAuthHeader(), }); dialogToggle(); } diff --git a/src/components/files/MineclusterFiles.jsx b/src/components/files/MineclusterFiles.jsx index 3dbc6d4..e41652e 100644 --- a/src/components/files/MineclusterFiles.jsx +++ b/src/components/files/MineclusterFiles.jsx @@ -18,6 +18,7 @@ import { getServerItem, } from "@mcl/queries"; import { previewServerItem } from "../../util/queries"; +import { cairoAuthHeader } from "@mcl/util/auth.js"; import { supportedFileTypes } from "./FilePreview.jsx"; @@ -109,6 +110,7 @@ export default function MineclusterFiles(props) { await fetch("/api/files/upload", { method: "POST", body: formData, + headers: cairoAuthHeader(), }); } diff --git a/src/nav/Viewport.jsx b/src/nav/Viewport.jsx index c981bc4..fabe10c 100644 --- a/src/nav/Viewport.jsx +++ b/src/nav/Viewport.jsx @@ -1,8 +1,5 @@ -import Box from "@mui/material/Box"; import Toolbar from "@mui/material/Toolbar"; import MCLPortal from "./MCLPortal.jsx"; -import Button from "@mui/material/Button"; -import SpeedDialIcon from "@mui/material/SpeedDialIcon"; // Import Navbar /*import Navbar from "./Navbar.jsx";*/ import { useCairoAuth } from "@mcl/util/auth.js"; diff --git a/src/util/auth.js b/src/util/auth.js index 1971cfa..fe3138b 100644 --- a/src/util/auth.js +++ b/src/util/auth.js @@ -31,26 +31,10 @@ export function useCairoAuth() { return auth; } -export function useAuth() { - const { state: settings } = useContext(SettingsContext); - const [auth, setAuth] = useState(!!!settings.cairoAuth); - - if (!settings.cairoAuth) return auth; - fetch("/api/auth/verify", { - headers: { Authorization: `Bearer ${settings.cairoAuth}` }, - }) - .then(() => setAuth(true)) - .catch(() => setAuth(false)); - - return auth; +export function getAuthTokenFromStorage() { + return JSON.parse(localStorage.getItem("settings")).cairoAuth; } -export function useUpdateAuth() { - const { updateSettings } = useContext(SettingsContext); - const [searchParams] = useSearchParams(); - const webToken = searchParams.get("cairoAuthToken"); - if (webToken) { - updateSettings({ cairoAuth: webToken }); - searchParams.delete("cairoAuthToken"); - } +export function cairoAuthHeader() { + return { Authorization: `Bearer ${getAuthTokenFromStorage()}` }; } diff --git a/src/util/queries.js b/src/util/queries.js index c80be01..14a7035 100644 --- a/src/util/queries.js +++ b/src/util/queries.js @@ -1,12 +1,17 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { cairoAuthHeader } from "@mcl/util/auth.js"; + const fetchApi = (subPath) => async () => - fetch(`/api${subPath}`).then((res) => res.json()); + fetch(`/api${subPath}`, { headers: cairoAuthHeader() }).then((res) => + res.json(), + ); const fetchApiCore = async (subPath, json, method = "POST", jsonify = false) => fetch(`/api${subPath}`, { method, headers: { "Content-Type": "application/json", + ...cairoAuthHeader(), }, body: JSON.stringify(json), }).then((res) => (jsonify ? res.json() : res)); @@ -16,6 +21,7 @@ const fetchApiPost = (subPath, json) => async () => method: "POST", headers: { "Content-Type": "application/json", + ...cairoAuthHeader(), }, body: JSON.stringify(json), }).then((res) => res.json()); @@ -117,6 +123,7 @@ const postJsonApi = (subPath, body, invalidate, method = "POST") => { method, headers: { "Content-Type": "application/json", + ...cairoAuthHeader(), }, body: JSON.stringify(body), });