From 8330890087e8f490549dd32791b3f0403ba944bb Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 4 Aug 2023 15:24:42 +0530 Subject: [PATCH] feat: implemented service token support for folder and secret import api --- .../controllers/v1/secretImportController.ts | 158 ++++++++++++-- .../controllers/v1/secretsFolderController.ts | 202 +++++++++++++----- backend/src/routes/v1/secretImport.ts | 12 +- backend/src/routes/v1/secretsFolder.ts | 20 +- 4 files changed, 303 insertions(+), 89 deletions(-) diff --git a/backend/src/controllers/v1/secretImportController.ts b/backend/src/controllers/v1/secretImportController.ts index 08f4929ff..368ffccff 100644 --- a/backend/src/controllers/v1/secretImportController.ts +++ b/backend/src/controllers/v1/secretImportController.ts @@ -1,15 +1,55 @@ import { Request, Response } from "express"; -import { validateMembership } from "../../helpers"; -import { Folder, SecretImport } from "../../models"; +import path from "path"; +import { isValidScope, validateMembership } from "../../helpers"; +import { ServiceTokenData } from "../../models"; +import Folder, { TFolderRootSchema } from "../../models/folder"; +import SecretImport from "../../models/secretImports"; +import { searchByFolderIdWithDir } from "../../services/FolderService"; import { getAllImportedSecrets } from "../../services/SecretImportService"; -import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; +import { BadRequestError, ResourceNotFoundError,UnauthorizedRequestError } from "../../utils/errors"; import { ADMIN, MEMBER } from "../../variables"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; import { getFolderPath } from "../../services/FolderService"; +const getFolderWithPathFromId = (folders: TFolderRootSchema, parentFolderId: string) => { + const search = searchByFolderIdWithDir(folders.nodes, parentFolderId); + if (!search) { + throw { message: "Folder permission denied" }; + } + const { folder, dir } = search; + const folderPath = path.join( + "/", + ...dir.filter(({ name }) => name !== "root").map(({ name }) => name) + ); + return { folder, folderPath, dir }; +}; + export const createSecretImport = async (req: Request, res: Response) => { const { workspaceId, environment, folderId, secretImport } = req.body; + + const folders = await Folder.findOne({ + workspace: workspaceId, + environment + }).lean(); + + if (!folders && folderId !== "root") { + throw BadRequestError({ message: "Folder doesn't exist" }); + } + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders, folderId); + secretPath = folderPath; + } + if (req.authData.authPayload instanceof ServiceTokenData) { + // root check + const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + const importSecDoc = await SecretImport.findOne({ workspace: workspaceId, environment, @@ -99,11 +139,34 @@ export const updateSecretImport = async (req: Request, res: Response) => { throw BadRequestError({ message: "Import not found" }); } - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: importSecDoc.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); + if (!(req.authData.authPayload instanceof ServiceTokenData)) { + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: importSecDoc.workspace, + acceptedRoles: [ADMIN, MEMBER] + }); + } else { + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders, importSecDoc.folderId); + secretPath = folderPath; + } + + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } const orderBefore = importSecDoc.imports; importSecDoc.imports = secretImports; @@ -149,11 +212,34 @@ export const deleteSecretImport = async (req: Request, res: Response) => { throw BadRequestError({ message: "Import not found" }); } - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: importSecDoc.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); + if (!(req.authData.authPayload instanceof ServiceTokenData)) { + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: importSecDoc.workspace, + acceptedRoles: [ADMIN, MEMBER] + }); + } else { + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders, importSecDoc.folderId); + secretPath = folderPath; + } + + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } importSecDoc.imports = importSecDoc.imports.filter( ({ environment, secretPath }) => !(environment === secretImportEnv && secretPath === secretImportPath) @@ -204,6 +290,29 @@ export const getSecretImports = async (req: Request, res: Response) => { return res.status(200).json({ secretImport: {} }); } + if (req.authData.authPayload instanceof ServiceTokenData) { + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders, importSecDoc.folderId); + secretPath = folderPath; + } + + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + return res.status(200).json({ secretImport: importSecDoc }); }; @@ -223,6 +332,29 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => { return res.status(200).json({ secrets: [] }); } + if (req.authData.authPayload instanceof ServiceTokenData) { + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders, importSecDoc.folderId); + secretPath = folderPath; + } + + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + await EEAuditLogService.createAuditLog( req.authData, { diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 334df6d4f..2cb74b55f 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -1,8 +1,12 @@ import { Request, Response } from "express"; -import { Secret } from "../../models"; import { Types } from "mongoose"; -import Folder from "../../models/folder"; -import { BadRequestError } from "../../utils/errors"; +import path from "path"; +import { EventType, FolderVersion } from "../../ee/models"; +import { EEAuditLogService, EESecretService } from "../../ee/services"; +import { validateMembership } from "../../helpers/membership"; +import { isValidScope } from "../../helpers/secrets"; +import { Secret, ServiceTokenData } from "../../models"; +import Folder, { TFolderRootSchema } from "../../models/folder"; import { appendFolder, deleteFolderById, @@ -11,31 +15,50 @@ import { getFolderByPath, getFolderPath, getParentFromFolderId, - searchByFolderId, searchByFolderIdWithDir, - validateFolderName, + validateFolderName } from "../../services/FolderService"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { ADMIN, MEMBER } from "../../variables"; -import { validateMembership } from "../../helpers/membership"; -import { EventType, FolderVersion } from "../../ee/models"; -import { EEAuditLogService, EESecretService } from "../../ee/services"; + +const getFolderWithPathFromId = (folders: TFolderRootSchema, parentFolderId: string) => { + const search = searchByFolderIdWithDir(folders.nodes, parentFolderId); + if (!search) { + throw { message: "Folder permission denied" }; + } + const { folder, dir } = search; + const folderPath = path.join( + "/", + ...dir.filter(({ name }) => name !== "root").map(({ name }) => name) + ); + return { folder, folderPath, dir }; +}; // verify workspace id/environment export const createFolder = async (req: Request, res: Response) => { const { workspaceId, environment, folderName, parentFolderId } = req.body; if (!validateFolderName(folderName)) { throw BadRequestError({ - message: "Folder name cannot contain spaces. Only underscore and dashes", + message: "Folder name cannot contain spaces. Only underscore and dashes" }); } const folders = await Folder.findOne({ workspace: workspaceId, - environment, + environment }).lean(); + // space has no folders initialized if (!folders) { + if (req.authData.authPayload instanceof ServiceTokenData) { + // root check + const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/"); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + const id = generateFolderId(); const folder = new Folder({ workspace: workspaceId, @@ -44,19 +67,19 @@ export const createFolder = async (req: Request, res: Response) => { id: "root", name: "root", version: 1, - children: [{ id, name: folderName, children: [], version: 1 }], - }, + children: [{ id, name: folderName, children: [], version: 1 }] + } }); await folder.save(); const folderVersion = new FolderVersion({ workspace: workspaceId, environment, - nodes: folder.nodes, + nodes: folder.nodes }); await folderVersion.save(); await EESecretService.takeSecretSnapshot({ workspaceId, - environment, + environment }); await EEAuditLogService.createAuditLog( @@ -81,20 +104,37 @@ export const createFolder = async (req: Request, res: Response) => { const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); await Folder.findByIdAndUpdate(folders._id, folders); - - const parentFolder = searchByFolderId(folders.nodes, parentFolderId); + const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId( + folders, + parentFolderId || "root" + ); + + if (req.authData.authPayload instanceof ServiceTokenData) { + // root check + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + environment, + parentFolderPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + + await Folder.findByIdAndUpdate(folders._id, folders); + const folderVersion = new FolderVersion({ workspace: workspaceId, environment, - nodes: parentFolder, + nodes: parentFolder }); await folderVersion.save(); await EESecretService.takeSecretSnapshot({ workspaceId, environment, - folderId: parentFolderId, + folderId: parentFolderId }); const folderPath = await getFolderPath(folders, folder.id); @@ -121,18 +161,25 @@ export const createFolder = async (req: Request, res: Response) => { export const updateFolderById = async (req: Request, res: Response) => { const { folderId } = req.params; const { name, workspaceId, environment } = req.body; + if (!validateFolderName(name)) { + throw BadRequestError({ + message: "Folder name cannot contain spaces. Only underscore and dashes" + }); + } const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (!folders) { throw BadRequestError({ message: "The folder doesn't exist" }); } - - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER], - }); + + if (!(req.authData.authPayload instanceof ServiceTokenData)) { + // check that user is a member of the workspace + await validateMembership({ + userId: req.user._id.toString(), + workspaceId, + acceptedRoles: [ADMIN, MEMBER] + }); + } const parentFolder = getParentFromFolderId(folders.nodes, folderId); if (!parentFolder) { @@ -144,7 +191,15 @@ export const updateFolderById = async (req: Request, res: Response) => { throw BadRequestError({ message: "The folder doesn't exist" }); } - const oldFolderName = folder.name; + if (req.authData.authPayload instanceof ServiceTokenData) { + const { folderPath: secretPath } = getFolderWithPathFromId(folders, parentFolder.id); + // root check + const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + parentFolder.version += 1; folder.name = name; @@ -152,14 +207,14 @@ export const updateFolderById = async (req: Request, res: Response) => { const folderVersion = new FolderVersion({ workspace: workspaceId, environment, - nodes: parentFolder, + nodes: parentFolder }); await folderVersion.save(); await EESecretService.takeSecretSnapshot({ workspaceId, environment, - folderId: parentFolder.id, + folderId: parentFolder.id }); const folderPath = await getFolderPath(folders, folder.id); @@ -183,7 +238,7 @@ export const updateFolderById = async (req: Request, res: Response) => { return res.json({ message: "Successfully updated folder", - folder: { name: folder.name, id: folder.id }, + folder: { name: folder.name, id: folder.id } }); }; @@ -196,12 +251,14 @@ export const deleteFolder = async (req: Request, res: Response) => { throw BadRequestError({ message: "The folder doesn't exist" }); } - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER], - }); + if (!(req.authData.authPayload instanceof ServiceTokenData)) { + // check that user is a member of the workspace + await validateMembership({ + userId: req.user._id.toString(), + workspaceId, + acceptedRoles: [ADMIN, MEMBER] + }); + } const folderPath = await getFolderPath(folders, folderId); @@ -211,6 +268,14 @@ export const deleteFolder = async (req: Request, res: Response) => { } const { deletedNode: delFolder, parent: parentFolder } = delOp; + if (req.authData.authPayload instanceof ServiceTokenData) { + const { folderPath: secretPath } = getFolderWithPathFromId(folders, parentFolder.id); + const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + parentFolder.version += 1; const delFolderIds = getAllFolderIds(delFolder); @@ -218,21 +283,21 @@ export const deleteFolder = async (req: Request, res: Response) => { const folderVersion = new FolderVersion({ workspace: workspaceId, environment, - nodes: parentFolder, + nodes: parentFolder }); await folderVersion.save(); if (delFolderIds.length) { await Secret.deleteMany({ folder: { $in: delFolderIds.map(({ id }) => id) }, workspace: workspaceId, - environment, + environment }); } await EESecretService.takeSecretSnapshot({ workspaceId, environment, - folderId: parentFolder.id, + folderId: parentFolder.id }); await EEAuditLogService.createAuditLog( @@ -256,13 +321,12 @@ export const deleteFolder = async (req: Request, res: Response) => { // TODO: validate workspace export const getFolders = async (req: Request, res: Response) => { - const { workspaceId, environment, parentFolderId, parentFolderPath } = - req.query as { - workspaceId: string; - environment: string; - parentFolderId?: string; - parentFolderPath?: string; - }; + const { workspaceId, environment, parentFolderId, parentFolderPath } = req.query as { + workspaceId: string; + environment: string; + parentFolderId?: string; + parentFolderPath?: string; + }; const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (!folders) { @@ -270,16 +334,29 @@ export const getFolders = async (req: Request, res: Response) => { return; } - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER], - }); + if (!(req.authData.authPayload instanceof ServiceTokenData)) { + // check that user is a member of the workspace + await validateMembership({ + userId: req.user._id.toString(), + workspaceId, + acceptedRoles: [ADMIN, MEMBER] + }); + } // if instead of parentFolderId given a path like /folder1/folder2 if (parentFolderPath) { + if (req.authData.authPayload instanceof ServiceTokenData) { + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + environment, + parentFolderPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } const folder = getFolderByPath(folders.nodes, parentFolderPath); + if (!folder) { res.send({ folders: [], dir: [] }); return; @@ -287,27 +364,36 @@ export const getFolders = async (req: Request, res: Response) => { // dir is not needed at present as this is only used in overview section of secrets res.send({ folders: folder.children.map(({ id, name }) => ({ id, name })), - dir: [{ name: folder.name, id: folder.id }], + dir: [{ name: folder.name, id: folder.id }] }); } if (!parentFolderId) { + if (req.authData.authPayload instanceof ServiceTokenData) { + const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/"); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + const rootFolders = folders.nodes.children.map(({ id, name }) => ({ id, - name, + name })); res.send({ folders: rootFolders }); return; } - const folderBySearch = searchByFolderIdWithDir(folders.nodes, parentFolderId); - if (!folderBySearch) { - throw BadRequestError({ message: "The folder doesn't exist" }); + const { folder, folderPath, dir } = getFolderWithPathFromId(folders, parentFolderId); + if (req.authData.authPayload instanceof ServiceTokenData) { + const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } } - const { folder, dir } = folderBySearch; res.send({ folders: folder.children.map(({ id, name }) => ({ id, name })), - dir, + dir }); }; diff --git a/backend/src/routes/v1/secretImport.ts b/backend/src/routes/v1/secretImport.ts index 79ee8e238..21973f57e 100644 --- a/backend/src/routes/v1/secretImport.ts +++ b/backend/src/routes/v1/secretImport.ts @@ -1,14 +1,14 @@ import express from "express"; -const router = express.Router(); import { body, param, query } from "express-validator"; import { secretImportController } from "../../controllers/v1"; import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { ADMIN, AuthMode, MEMBER } from "../../variables"; +const router = express.Router(); router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT] + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -27,7 +27,7 @@ router.post( router.put( "/:id", requireAuth({ - acceptedAuthModes: [AuthMode.JWT] + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), param("id").exists().isString().trim(), body("secretImports").exists().isArray(), @@ -40,7 +40,7 @@ router.put( router.delete( "/:id", requireAuth({ - acceptedAuthModes: [AuthMode.JWT] + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), param("id").exists().isString().trim(), body("secretImportPath").isString().exists().trim(), @@ -52,7 +52,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT] + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -68,7 +68,7 @@ router.get( router.get( "/secrets", requireAuth({ - acceptedAuthModes: [AuthMode.JWT] + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index f7773367b..4580e50d3 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -1,27 +1,23 @@ import express from "express"; -const router = express.Router(); -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; import { body, param, query } from "express-validator"; import { createFolder, deleteFolder, getFolders, - updateFolderById, + updateFolderById } from "../../controllers/v1/secretsFolderController"; +import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { ADMIN, AuthMode, MEMBER } from "../../variables"; +const router = express.Router(); router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", + locationWorkspaceId: "body" }), body("workspaceId").exists(), body("environment").exists(), @@ -34,7 +30,7 @@ router.post( router.patch( "/:folderId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), body("workspaceId").exists(), body("environment").exists(), @@ -46,7 +42,7 @@ router.patch( router.delete( "/:folderId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), body("workspaceId").exists(), body("environment").exists(), @@ -58,7 +54,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] }), query("workspaceId").exists().isString().trim(), query("environment").exists().isString().trim(),