diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index f7e3cc6b2..51c93182b 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -25,6 +25,8 @@ import { BatchSecretRequest, BatchSecret } from '../../types/secret'; +import { getFolderPath, getFoldersInDirectory, normalizePath } from '../../utils/folder'; +import { ROOT_FOLDER_PATH } from '../../utils/folder'; /** * Peform a batch of any specified CUD secret operations @@ -51,13 +53,18 @@ export const batchSecrets = async (req: Request, res: Response) => { const updateSecrets: BatchSecret[] = []; const deleteSecrets: Types.ObjectId[] = []; const actions: IAction[] = []; - + // get secret blind index salt const salt = await SecretService.getSecretBlindIndexSalt({ workspaceId: new Types.ObjectId(workspaceId) }); for await (const request of requests) { + const folderId = request.secret.folderId + + // TODO: need to auth folder + const fullFolderPath = await getFolderPath(folderId) + let secretBlindIndex = ''; switch (request.method) { case 'POST': @@ -72,19 +79,23 @@ export const batchSecrets = async (req: Request, res: Response) => { user: request.secret.type === SECRET_PERSONAL ? req.user : undefined, environment, workspace: new Types.ObjectId(workspaceId), + path: fullFolderPath, + folder: folderId, secretBlindIndex }); break; case 'PATCH': secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ secretName: request.secret.secretName, - salt + salt, }); updateSecrets.push({ ...request.secret, _id: new Types.ObjectId(request.secret._id), - secretBlindIndex + secretBlindIndex, + folder: folderId, + path: fullFolderPath, }); break; case 'DELETE': @@ -437,9 +448,9 @@ export const createSecrets = async (req: Request, res: Response) => { }); }) ); - + const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject()); - + setTimeout(async () => { // trigger event - push secrets await EventService.handleEvent({ @@ -578,9 +589,11 @@ export const getSecrets = async (req: Request, res: Response) => { } */ - const { tagSlugs } = req.query; + const { tagSlugs, secretsPath } = req.query; const workspaceId = req.query.workspaceId as string; const environment = req.query.environment as string; + const normalizedPath = normalizePath(secretsPath as string) + const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath) // secrets to return let secrets: ISecret[] = []; @@ -613,6 +626,12 @@ export const getSecrets = async (req: Request, res: Response) => { ] } + if (normalizedPath == ROOT_FOLDER_PATH) { + secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } + } else if (normalizedPath) { + secretQuery.path = normalizedPath + } + if (tagIds.length > 0) { secretQuery.tags = { $in: tagIds }; } @@ -638,6 +657,13 @@ export const getSecrets = async (req: Request, res: Response) => { ] } + // TODO: check if user can query for given path + if (normalizedPath == ROOT_FOLDER_PATH) { + secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } + } else if (normalizedPath) { + secretQuery.path = normalizedPath + } + if (tagIds.length > 0) { secretQuery.tags = { $in: tagIds }; } @@ -655,6 +681,12 @@ export const getSecrets = async (req: Request, res: Response) => { user: { $exists: false } // shared secrets only from workspace } + if (normalizedPath == ROOT_FOLDER_PATH) { + secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } + } else if (normalizedPath) { + secretQuery.path = normalizedPath + } + if (tagIds.length > 0) { secretQuery.tags = { $in: tagIds }; } @@ -701,7 +733,8 @@ export const getSecrets = async (req: Request, res: Response) => { } return res.status(200).send({ - secrets + secrets, + folders }); } diff --git a/backend/src/index.ts b/backend/src/index.ts index c278da13f..7270470ce 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -44,7 +44,8 @@ import { password as v1PasswordRouter, stripe as v1StripeRouter, integration as v1IntegrationRouter, - integrationAuth as v1IntegrationAuthRouter + integrationAuth as v1IntegrationAuthRouter, + secretsFolder as v1SecretsFolder } from './routes/v1'; import { signup as v2SignupRouter, @@ -137,6 +138,7 @@ const main = async () => { app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/integration', v1IntegrationRouter); app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); + app.use('/api/v1/folder', v1SecretsFolder) // v2 routes (improvements) app.use('/api/v2/signup', v2SignupRouter); @@ -151,7 +153,7 @@ const main = async () => { app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new app.use('/api/v2/api-key', v2APIKeyDataRouter); - + // v3 routes (experimental) app.use('/api/v3/secrets', v3SecretsRouter); app.use('/api/v3/workspaces', v3WorkspacesRouter); diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 725741ccf..7670124bf 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -3,6 +3,7 @@ import { SECRET_SHARED, SECRET_PERSONAL, } from '../variables'; +import { ROOT_FOLDER_PATH } from '../utils/folder'; export interface ISecret { _id: Types.ObjectId; @@ -25,6 +26,8 @@ export interface ISecret { secretCommentTag?: string; secretCommentHash?: string; tags?: string[]; + path?: string; + folder?: Types.ObjectId; } const secretSchema = new Schema( @@ -107,7 +110,18 @@ const secretSchema = new Schema( secretCommentHash: { type: String, required: false - } + }, + // the full path to the secret in relation to folders + path: { + type: String, + required: false, + default: ROOT_FOLDER_PATH + }, + folder: { + type: Schema.Types.ObjectId, + ref: 'Folder', + required: false, + }, }, { timestamps: true diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 2dfe58baa..4b9dcafca 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -15,6 +15,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; +import secretsFolder from './secretsFolder' export { signup, @@ -33,5 +34,6 @@ export { password, stripe, integration, - integrationAuth + integrationAuth, + secretsFolder }; diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts new file mode 100644 index 000000000..7f2cd1bc8 --- /dev/null +++ b/backend/src/routes/v1/secretsFolder.ts @@ -0,0 +1,40 @@ +import express, { Request, Response } from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../middleware'; +import { body, param } from 'express-validator'; +import { createFolder, deleteFolder } from '../../controllers/v1/secretsFolderController'; +import { ADMIN, MEMBER } from '../../variables'; + +router.post( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body' + }), + body('workspaceId').exists(), + body('environment').exists(), + body('folderName').exists(), + body('parentFolderId'), + validateRequest, + createFolder +); + +router.delete( + '/:folderId', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + param('folderId').exists(), + validateRequest, + deleteFolder +); + + +export default router; \ No newline at end of file