diff --git a/backend/src/app.ts b/backend/src/app.ts index 9fba18c67..4b02e0311 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,6 +14,7 @@ import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, secret as eeSecretRouter, + secretSnapshot as eeSecretSnapshotRouter, action as eeActionRouter } from './ee/routes/v1'; import { @@ -69,6 +70,7 @@ if (NODE_ENV === 'production') { // (EE) routes app.use('/api/v1/secret', eeSecretRouter); +app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter); app.use('/api/v1/workspace', eeWorkspaceRouter); app.use('/api/v1/action', eeActionRouter); diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index 2a082de70..dd88f1178 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -1,11 +1,13 @@ import * as stripeController from './stripeController'; import * as secretController from './secretController'; +import * as secretSnapshotController from './secretSnapshotController'; import * as workspaceController from './workspaceController'; import * as actionController from './actionController'; export { stripeController, secretController, + secretSnapshotController, workspaceController, actionController } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts new file mode 100644 index 000000000..40e1a74a6 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -0,0 +1,27 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretSnapshot } from '../../models'; + +export const getSecretSnapshot = async (req: Request, res: Response) => { + let secretSnapshot; + try { + const { secretSnapshotId } = req.params; + + secretSnapshot = await SecretSnapshot + .findById(secretSnapshotId) + .populate('secretVersions'); + + if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret snapshot' + }); + } + + return res.status(200).send({ + secretSnapshot + }); +} \ No newline at end of file diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index 2f726a9a5..529c9a980 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -23,34 +23,44 @@ import { }: { workspaceId: string; }) => { + let secretSnapshot; try { - const secrets = await Secret.find({ + const secretIds = (await Secret.find({ workspace: workspaceId - }); + }, '_id')).map((s) => s._id); + const latestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds + } + } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' }, + versionId: { $max: '$_id' } // secret version id + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()) + .map((s) => s.versionId); + const latestSecretSnapshot = await SecretSnapshot.findOne({ workspace: workspaceId }).sort({ version: -1 }); - if (!latestSecretSnapshot) { - // case: no snapshots exist for workspace -> create first snapshot - await new SecretSnapshot({ - workspace: workspaceId, - version: 1, - secrets - }).save(); - - return; - } - - // case: snapshots exist for workspace secretSnapshot = await new SecretSnapshot({ workspace: workspaceId, - version: latestSecretSnapshot.version + 1, - secrets + version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, + secretVersions: latestSecretVersions }).save(); - } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/ee/middleware/index.ts b/backend/src/ee/middleware/index.ts new file mode 100644 index 000000000..ff9267965 --- /dev/null +++ b/backend/src/ee/middleware/index.ts @@ -0,0 +1,7 @@ +import requireLicenseAuth from './requireLicenseAuth'; +import requireSecretSnapshotAuth from './requireSecretSnapshotAuth'; + +export { + requireLicenseAuth, + requireSecretSnapshotAuth +} \ No newline at end of file diff --git a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts new file mode 100644 index 000000000..71d7c5215 --- /dev/null +++ b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts @@ -0,0 +1,50 @@ +import { Request, Response, NextFunction } from 'express'; +import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors'; +import { SecretSnapshot } from '../models'; +import { + validateMembership +} from '../../helpers/membership'; + +/** + * Validate if user on request has proper membership for secret snapshot + * @param {Object} obj + * @param {String[]} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.acceptedStatuses - accepted workspace statuses + * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing + */ +const requireSecretSnapshotAuth = ({ + acceptedRoles, + acceptedStatuses +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + const { secretSnapshotId } = req.params; + + const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId); + + if (!secretSnapshot) { + return next(SecretSnapshotNotFoundError({ + message: 'Failed to find secret snapshot' + })); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: secretSnapshot.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.secretSnapshot = secretSnapshot as any; + + next(); + } catch (err) { + return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' })); + } + } +} + +export default requireSecretSnapshotAuth; \ No newline at end of file diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts index 69633a92e..c646f353a 100644 --- a/backend/src/ee/models/secretSnapshot.ts +++ b/backend/src/ee/models/secretSnapshot.ts @@ -1,31 +1,9 @@ import { Schema, model, Types } from 'mongoose'; -import { - SECRET_SHARED, - SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD -} from '../../variables'; export interface ISecretSnapshot { workspace: Types.ObjectId; version: number; - secrets: { - version: number; - workspace: Types.ObjectId; - type: string; - user: Types.ObjectId; - environment: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretKeyHash: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretValueHash: string; - }[] + secretVersions: Types.ObjectId[]; } const secretSnapshotSchema = new Schema( @@ -39,64 +17,10 @@ const secretSnapshotSchema = new Schema( type: Number, required: true }, - secrets: [{ - version: { - type: Number, - default: 1, - required: true - }, - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - type: { - type: String, - enum: [SECRET_SHARED, SECRET_PERSONAL], - required: true - }, - user: { - // user associated with the personal secret - type: Schema.Types.ObjectId, - ref: 'User' - }, - environment: { - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], - required: true - }, - secretKeyCiphertext: { - type: String, - required: true - }, - secretKeyIV: { - type: String, // symmetric - required: true - }, - secretKeyTag: { - type: String, // symmetric - required: true - }, - secretKeyHash: { - type: String, - required: true - }, - secretValueCiphertext: { - type: String, - required: true - }, - secretValueIV: { - type: String, // symmetric - required: true - }, - secretValueTag: { - type: String, // symmetric - required: true - }, - secretValueHash: { - type: String, - required: true - } + secretVersions: [{ + type: Schema.Types.ObjectId, + ref: 'SecretVersion', + required: true }] }, { diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index a93a037f6..0197c3a25 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -1,9 +1,30 @@ import { Schema, model, Types } from 'mongoose'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD +} from '../../variables'; + +/** + * TODO: + * 1. Modify SecretVersion to also contain XX + * - type + * - user + * - environment + * 2. Modify SecretSnapshot to point to arrays of SecretVersion + */ export interface ISecretVersion { _id?: Types.ObjectId; secret: Types.ObjectId; version: number; + workspace: Types.ObjectId; // new + type: string; // new + user: Types.ObjectId; // new + environment: string; // new isDeleted: boolean; secretKeyCiphertext: string; secretKeyIV: string; @@ -27,6 +48,26 @@ const secretVersionSchema = new Schema( default: 1, required: true }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: 'User' + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, isDeleted: { type: Boolean, default: false, diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 02fc80939..612715111 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,9 +1,11 @@ import secret from './secret'; +import secretSnapshot from './secretSnapshot'; import workspace from './workspace'; import action from './action'; export { secret, + secretSnapshot, workspace, action } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 7217c96e2..cb6897994 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -5,7 +5,7 @@ import { requireSecretAuth, validateRequest } from '../../../middleware'; -import { body, query, param } from 'express-validator'; +import { query, param } from 'express-validator'; import { secretController } from '../../controllers/v1'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts new file mode 100644 index 000000000..04f6c9d59 --- /dev/null +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -0,0 +1,26 @@ +import express from 'express'; +const router = express.Router(); +import { + requireSecretSnapshotAuth +} from '../../middleware'; +import { + requireAuth, + validateRequest +} from '../../../middleware'; +import { param } from 'express-validator'; +import { ADMIN, MEMBER, GRANTED } from '../../../variables'; +import { secretSnapshotController } from '../../controllers/v1'; + +router.get( + '/:secretSnapshotId', + requireAuth, + requireSecretSnapshotAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('secretSnapshotId').exists().trim(), + validateRequest, + secretSnapshotController.getSecretSnapshot +); + +export default router; \ No newline at end of file diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index f71f094e8..0d64da3b1 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Secret, ISecret, @@ -8,7 +9,6 @@ import { EELogService } from '../ee/services'; import { - SecretVersion, IAction } from '../ee/models'; import { @@ -104,11 +104,9 @@ const v1PushSecrets = async ({ await Secret.deleteMany({ _id: { $in: toDelete } }); - - await SecretVersion.updateMany({ - secret: { $in: toDelete } - }, { - isDeleted: true + + await EESecretService.markDeletedSecretVersions({ + secretIds: toDelete }); } @@ -191,6 +189,10 @@ const v1PushSecrets = async ({ return ({ secret: _id, version: version ? version + 1 : 1, + workspace: new Types.ObjectId(workspaceId), + type: newSecret.type, + user: new Types.ObjectId(userId), + environment, isDeleted: false, secretKeyCiphertext: newSecret.ciphertextKey, secretKeyIV: newSecret.ivKey, @@ -242,6 +244,11 @@ const v1PushSecrets = async ({ EESecretService.addSecretVersions({ secretVersions: newSecrets.map(({ _id, + version, + workspace, + type, + user, + environment, secretKeyCiphertext, secretKeyIV, secretKeyTag, @@ -252,7 +259,11 @@ const v1PushSecrets = async ({ secretValueHash }) => ({ secret: _id, - version: 1, + version, + workspace, + type, + user, + environment, isDeleted: false, secretKeyCiphertext, secretKeyIV, @@ -419,29 +430,14 @@ const v1PushSecrets = async ({ // (EE) add secret versions for updated secrets await EESecretService.addSecretVersions({ secretVersions: toUpdate.map((s) => { - const { - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; - return ({ + ...newSecretsObj[`${s.type}-${s.secretKeyHash}`], secret: s._id, version: s.version ? s.version + 1 : 1, - isDeleted: false, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash + workspace: new Types.ObjectId(workspaceId), + user: s.user, + environment: s.environment, + isDeleted: false }) }) }); @@ -474,31 +470,13 @@ const v1PushSecrets = async ({ // (EE) add secret versions for new secrets EESecretService.addSecretVersions({ - secretVersions: newSecrets.map(({ - _id, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash - }) => ({ - secret: _id, - version: 1, - isDeleted: false, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash + secretVersions: newSecrets.map((s) => ({ + ...s, + secret: s._id, + isDeleted: false })) }); - + const addAction = await EELogService.createActionSecret({ name: ACTION_ADD_SECRETS, userId, diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 68961a946..33b10e2bc 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -13,6 +13,7 @@ declare global { integrationAuth: any; bot: any; secret: any; + secretSnapshot: any; serviceToken: any; accessToken: any; query?: any; diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index ba5611465..3c6a385e5 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -123,6 +123,16 @@ export const SecretNotFoundError = (error?: Partial) => new stack: error?.stack }); +//* ----->[SECRET SNAPSHOT ERRORS]<----- +export const SecretSnapshotNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'secret_snapshot_not_found_error', + message: error?.message ?? 'The requested secret snapshot was not found', + context: error?.context, + stack: error?.stack +}); + //* ----->[ACTION ERRORS]<----- export const ActionNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR,