diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 26c9cc7a8..ae1de8cfe 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,6 +1,6 @@ import { Types } from "mongoose"; import { Request, Response } from "express"; -import { ISecret, Secret } from "../../models"; +import { ISecret, Secret, ServiceTokenData } from "../../models"; import { IAction, SecretVersion } from "../../ee/models"; import { SECRET_PERSONAL, @@ -29,6 +29,7 @@ import { BatchSecretRequest, BatchSecret } from "../../types/secret"; import Folder from "../../models/folder"; import { getFolderByPath, + getFolderIdFromServiceToken, searchByFolderId, } from "../../services/FolderService"; @@ -45,14 +46,15 @@ export const batchSecrets = async (req: Request, res: Response) => { const { workspaceId, environment, - folderId, requests, + secretPath, }: { workspaceId: string; environment: string; - folderId: string; requests: BatchSecretRequest[]; + secretPath: string; } = req.body; + let folderId = req.body.folderId as string; const createSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = []; @@ -70,6 +72,25 @@ export const batchSecrets = async (req: Request, res: Response) => { if (!folder) throw BadRequestError({ message: "Folder not found" }); } + if (req.authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; + // in service token when not giving secretpath folderid must be root + // this is to avoid giving folderid when service tokens are used + if ( + (!secretPath && folderId !== "root") || + (secretPath && secretPath !== serviceTkScopedSecretPath) + ) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + if (secretPath) { + folderId = await getFolderIdFromServiceToken( + workspaceId, + environment, + secretPath + ); + } + for await (const request of requests) { // do a validation @@ -152,6 +173,7 @@ export const batchSecrets = async (req: Request, res: Response) => { numberOfSecrets: createdSecrets.length, environment, workspaceId, + folderId, channel, userAgent: req.headers?.["user-agent"], }, @@ -218,7 +240,7 @@ export const batchSecrets = async (req: Request, res: Response) => { algorithm: ALGORITHM_AES_256_GCM, keyEncoding: ENCODING_SCHEME_UTF8, tags: u.tags, - folder: u.folder + folder: u.folder, }) ); @@ -248,6 +270,7 @@ export const batchSecrets = async (req: Request, res: Response) => { numberOfSecrets: updateSecrets.length, environment, workspaceId, + folderId, channel, userAgent: req.headers?.["user-agent"], }, @@ -395,8 +418,13 @@ export const createSecrets = async (req: Request, res: Response) => { const { workspaceId, environment, - folderId, - }: { workspaceId: string; environment: string; folderId: string } = req.body; + secretPath, + }: { + workspaceId: string; + environment: string; + secretPath?: string; + } = req.body; + let folderId = req.body.folderId; if (req.user) { const hasAccess = await userHasWorkspaceAccess( @@ -421,6 +449,24 @@ export const createSecrets = async (req: Request, res: Response) => { // case: create 1 secret listOfSecretsToCreate = [req.body.secrets]; } + if (req.authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; + // in service token when not giving secretpath folderid must be root + // this is to avoid giving folderid when service tokens are used + if ( + (!secretPath && folderId !== "root") || + (secretPath && secretPath !== serviceTkScopedSecretPath) + ) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + if (secretPath) { + folderId = await getFolderIdFromServiceToken( + workspaceId, + environment, + secretPath + ); + } // get secret blind index salt const salt = await SecretService.getSecretBlindIndexSalt({ @@ -585,6 +631,7 @@ export const createSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel: channel, + folderId, userAgent: req.headers?.["user-agent"], }, }); @@ -660,6 +707,18 @@ export const getSecrets = async (req: Request, res: Response) => { if (!folder) throw BadRequestError({ message: "Folder not found" }); } + if (req.authData.authPayload instanceof ServiceTokenData) { + const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; + // in service token when not giving secretpath folderid must be root + // this is to avoid giving folderid when service tokens are used + if ( + (!secretPath && folderId !== "root") || + (secretPath && secretPath !== serviceTkScopedSecretPath) + ) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } + if (folders && secretPath) { if (!folders) throw BadRequestError({ message: "Folder not found" }); const folder = getFolderByPath(folders.nodes, secretPath as string); @@ -800,6 +859,7 @@ export const getSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel, + folderId, userAgent: req.headers?.["user-agent"], }, }); @@ -910,13 +970,13 @@ export const updateSecrets = async (req: Request, res: Response) => { keyEncoding: ENCODING_SCHEME_UTF8, tags, ...(secretCommentCiphertext !== undefined && - secretCommentIV && - secretCommentTag + secretCommentIV && + secretCommentTag ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - } + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + } : {}), }, }, diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 99c3a4fa9..7b866449e 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -44,30 +44,7 @@ import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj, } from "../utils/auth"; -import Folder from "../models/folder"; -import { getFolderByPath } from "../services/FolderService"; - -export const getFolderIdFromServiceToken = async ( - workspaceId: Types.ObjectId | string, - environment: string, - secretPath: string -) => { - const folders = await Folder.findOne({ - workspace: workspaceId, - environment, - }); - - if (!folders) { - if (secretPath !== "/") throw new Error("Invalid path. Folders not found"); - } else { - const folder = getFolderByPath(folders.nodes, secretPath); - if (!folder) { - throw new Error("Folder not found"); - } - return folder.id; - } - return "root"; -}; +import { getFolderIdFromServiceToken } from "../services/FolderService"; /** * Create secret blind index data containing encrypted blind index [salt] diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index cef804882..3cf7e83d5 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -1,15 +1,15 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { requireAuth, requireWorkspaceAuth, requireSecretsAuth, validateRequest, -} from '../../middleware'; -import { validateClientForSecrets } from '../../validation'; -import { query, body } from 'express-validator'; -import { secretsController } from '../../controllers/v2'; +} from "../../middleware"; +import { validateClientForSecrets } from "../../validation"; +import { query, body } from "express-validator"; +import { secretsController } from "../../controllers/v2"; import { ADMIN, MEMBER, @@ -21,11 +21,11 @@ import { AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY, -} from '../../variables'; -import { BatchSecretRequest } from '../../types/secret'; +} from "../../variables"; +import { BatchSecretRequest } from "../../types/secret"; router.post( - '/batch', + "/batch", requireAuth({ acceptedAuthModes: [ AUTH_MODE_JWT, @@ -35,12 +35,13 @@ router.post( }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body', + locationWorkspaceId: "body", }), - body('workspaceId').exists().isString().trim(), - body('folderId').default('root').isString().trim(), - body('environment').exists().isString().trim(), - body('requests') + body("workspaceId").exists().isString().trim(), + body("folderId").default("root").isString().trim(), + body("environment").exists().isString().trim(), + body("secretPath").optional().isString().trim(), + body("requests") .exists() .custom(async (requests: BatchSecretRequest[], { req }) => { if (Array.isArray(requests)) { @@ -65,17 +66,18 @@ router.post( ); router.post( - '/', - body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim(), - body('folderId').default('root').isString().trim(), - body('secrets') + "/", + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("folderId").default("root").isString().trim(), + body("secretPath").optional().isString().trim(), + body("secrets") .exists() .custom((value) => { if (Array.isArray(value)) { // case: create multiple secrets if (value.length === 0) - throw new Error('secrets cannot be an empty array'); + throw new Error("secrets cannot be an empty array"); for (const secret of value) { if ( !secret.type || @@ -85,16 +87,16 @@ router.post( !secret.secretKeyCiphertext || !secret.secretKeyIV || !secret.secretKeyTag || - typeof secret.secretValueCiphertext !== 'string' || + typeof secret.secretValueCiphertext !== "string" || !secret.secretValueIV || !secret.secretValueTag ) { throw new Error( - 'secrets array must contain objects that have required secret properties' + "secrets array must contain objects that have required secret properties" ); } } - } else if (typeof value === 'object') { + } else if (typeof value === "object") { // case: update 1 secret if ( !value.type || @@ -107,11 +109,11 @@ router.post( !value.secretValueTag ) { throw new Error( - 'secrets object is missing required secret properties' + "secrets object is missing required secret properties" ); } } else { - throw new Error('secrets must be an object or an array of objects'); + throw new Error("secrets must be an object or an array of objects"); } return true; @@ -126,19 +128,20 @@ router.post( }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body', - locationEnvironment: 'body', + locationWorkspaceId: "body", + locationEnvironment: "body", requiredPermissions: [PERMISSION_WRITE_SECRETS], }), secretsController.createSecrets ); router.get( - '/', - query('workspaceId').exists().trim(), - query('environment').exists().trim(), - query('tagSlugs'), - query('folderId').default('root').isString().trim(), + "/", + query("workspaceId").exists().trim(), + query("environment").exists().trim(), + query("tagSlugs"), + query("folderId").default("root").isString().trim(), + query("secretPath").optional().isString().trim(), validateRequest, requireAuth({ acceptedAuthModes: [ @@ -150,34 +153,34 @@ router.get( }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'query', - locationEnvironment: 'query', + locationWorkspaceId: "query", + locationEnvironment: "query", requiredPermissions: [PERMISSION_READ_SECRETS], }), secretsController.getSecrets ); router.patch( - '/', - body('secrets') + "/", + body("secrets") .exists() .custom((value) => { if (Array.isArray(value)) { // case: update multiple secrets if (value.length === 0) - throw new Error('secrets cannot be an empty array'); + throw new Error("secrets cannot be an empty array"); for (const secret of value) { if (!secret.id) { - throw new Error('Each secret must contain a ID property'); + throw new Error("Each secret must contain a ID property"); } } - } else if (typeof value === 'object') { + } else if (typeof value === "object") { // case: update 1 secret if (!value.id) { - throw new Error('secret must contain a ID property'); + throw new Error("secret must contain a ID property"); } } else { - throw new Error('secrets must be an object or an array of objects'); + throw new Error("secrets must be an object or an array of objects"); } return true; @@ -198,21 +201,21 @@ router.patch( ); router.delete( - '/', - body('secretIds') + "/", + body("secretIds") .exists() .custom((value) => { // case: delete 1 secret - if (typeof value === 'string') return true; + if (typeof value === "string") return true; if (Array.isArray(value)) { // case: delete multiple secrets if (value.length === 0) - throw new Error('secrets cannot be an empty array'); - return value.every((id: string) => typeof id === 'string'); + throw new Error("secrets cannot be an empty array"); + return value.every((id: string) => typeof id === "string"); } - throw new Error('secretIds must be a string or an array of strings'); + throw new Error("secretIds must be a string or an array of strings"); }) .not() .isEmpty(), diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index 9344612c1..f2dfb3050 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -1,5 +1,6 @@ import { nanoid } from "nanoid"; -import { TFolderSchema } from "../models/folder"; +import { Types } from "mongoose"; +import Folder, { TFolderSchema } from "../models/folder"; type TAppendFolderDTO = { folderName: string; @@ -192,3 +193,25 @@ export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => { } return segment; }; + +export const getFolderIdFromServiceToken = async ( + workspaceId: Types.ObjectId | string, + environment: string, + secretPath: string +) => { + const folders = await Folder.findOne({ + workspace: workspaceId, + environment, + }); + + if (!folders) { + if (secretPath !== "/") throw new Error("Invalid path. Folders not found"); + } else { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw new Error("Folder not found"); + } + return folder.id; + } + return "root"; +};