From e2139882dac5e01891c4d33636fe7e13a8e64fe6 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 12 Apr 2023 14:17:20 +0300 Subject: [PATCH] Update middleware for requireSecretAuth --- backend/src/ee/routes/v1/secret.ts | 13 ++- backend/src/helpers/membership.ts | 2 +- backend/src/helpers/secrets.ts | 109 +++++++++++++++++-- backend/src/helpers/user.ts | 42 ++++++- backend/src/middleware/requireSecretAuth.ts | 28 ++--- backend/src/middleware/requireSecretsAuth.ts | 3 +- backend/src/routes/v2/secret.ts | 10 +- backend/src/routes/v2/secrets.ts | 3 +- 8 files changed, 175 insertions(+), 35 deletions(-) diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 8e93919a0..3e956a388 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -7,7 +7,12 @@ import { } from '../../../middleware'; import { query, param, body } from 'express-validator'; import { secretController } from '../../controllers/v1'; -import { ADMIN, MEMBER } from '../../../variables'; +import { + ADMIN, + MEMBER, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from '../../../variables'; router.get( '/:secretId/secret-versions', @@ -15,7 +20,8 @@ router.get( acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS] }), param('secretId').exists().trim(), query('offset').exists().isInt(), @@ -30,7 +36,8 @@ router.post( acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] }), param('secretId').exists().trim(), body('version').exists().isInt(), diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 8ae31812d..503ca9fc6 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -131,7 +131,7 @@ const validateMembership = async ({ if (acceptedRoles) { if (!acceptedRoles.includes(membership.role)) { - throw BadRequestError({ message: 'Failed to validate workspace membership role' }); + throw BadRequestError({ message: 'Failed authorization for membership role' }); } } diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 09418560d..7d3e54105 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -10,15 +10,24 @@ import { ISecret } from '../models'; import { + validateMembership +} from '../helpers/membership'; +import { + validateUserClientForSecret, validateUserClientForSecrets } from '../helpers/user'; import { - validateServiceTokenDataClientForSecrets + validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData'; import { - validateServiceAccountClientForSecrets + validateServiceAccountClientForSecrets, + validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { + BadRequestError, + UnauthorizedRequestError, + SecretNotFoundError +} from '../utils/errors'; import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, @@ -27,12 +36,91 @@ import { } from '../variables'; /** - * Validate accepted clients for secrets with ids [secretIds] + * Validate authenticated clients for secrets with id [secretId] based + * on any known permissions. * @param {Object} obj - * @param {User} obj.user - user client - * @param {ServiceAccount} obj.serviceAccount - service account client - * @param {ServiceTokenData} obj.service - service token client - * @param {String[]} obj.secretIds - ids of secrets to validate against + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.secretId - id of secret to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +const validateClientForSecret = async ({ + authData, + secretId, + acceptedRoles, + requiredPermissions +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, + secretId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions: string[]; +}) => { + const secret = await Secret.findById(secretId); + + if (!secret) throw SecretNotFoundError({ + message: 'Failed to find secret' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for secret' + }); +} + +/** + * Validate authenticated clients for secrets with ids [secretIds] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against + * @param {String} obj.environment - (optional) environment in workspace to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ const validateClientForSecrets = async ({ authData, @@ -43,7 +131,7 @@ const validateClientForSecrets = async ({ authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }, - secretIds: string[]; + secretIds: Types.ObjectId[]; requiredPermissions: string[]; }) => { @@ -51,7 +139,7 @@ const validateClientForSecrets = async ({ secrets = await Secret.find({ _id: { - $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) + $in: secretIds } }); @@ -105,5 +193,6 @@ const validateClientForSecrets = async ({ } export { + validateClientForSecret, validateClientForSecrets } \ No newline at end of file diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 7a0c52cb7..5a33f3d2e 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -219,6 +219,42 @@ const validateUserClientForWorkspace = async ({ return membership; } +/** + * Validate that user (client) can access secret [secret] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Secret[]} obj.secrets - secrets to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateUserClientForSecret = async ({ + user, + secret, + acceptedRoles, + requiredPermissions +}: { + user: IUser; + secret: ISecret; + acceptedRoles?: Array<'admin' | 'member'>; + requiredPermissions?: string[]; +}) => { + const membership = await validateMembership({ + userId: user._id, + workspaceId: secret.workspace, + acceptedRoles + }); + + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: 'You do not have the required permissions to perform this action' + }); + } + } +} + /** * Validate that user (client) can access secrets [secrets] * with required permissions [requiredPermissions] @@ -236,7 +272,8 @@ const validateUserClientForWorkspace = async ({ secrets: ISecret[]; requiredPermissions?: string[]; }) => { - // TODO: refactor + + // TODO: add acceptedRoles? const userMemberships = await Membership.find({ user: user._id }) const userMembershipById = _.keyBy(userMemberships, 'workspace'); @@ -326,5 +363,6 @@ export { validateUserClientForWorkspace, validateUserClientForSecrets, validateUserClientForServiceAccount, - validateUserClientForOrganization + validateUserClientForOrganization, + validateUserClientForSecret }; diff --git a/backend/src/middleware/requireSecretAuth.ts b/backend/src/middleware/requireSecretAuth.ts index c131e624f..1462d67b0 100644 --- a/backend/src/middleware/requireSecretAuth.ts +++ b/backend/src/middleware/requireSecretAuth.ts @@ -1,12 +1,17 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; import { Secret } from '../models'; import { validateMembership } from '../helpers/membership'; +import { + validateClientForSecret +} from '../helpers/secrets'; // note: used for old /v1/secret and /v2/secret routes. -// newer /v2/secrets routes use [requireSecretsAuth] middleware +// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception +// of some /ee endpoints /** * Validate if user on request has proper membership to modify secret. @@ -15,25 +20,20 @@ import { * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireSecretAuth = ({ - acceptedRoles + acceptedRoles, + requiredPermissions }: { acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { secretId } = req.params; - const secret = await Secret.findById(secretId); - - if (!secret) { - return next(SecretNotFoundError({ - message: 'Failed to find secret' - })); - } - - await validateMembership({ - userId: req.user._id, - workspaceId: secret.workspace, - acceptedRoles + const secret = await validateClientForSecret({ + authData: req.authData, + secretId: new Types.ObjectId(secretId), + acceptedRoles, + requiredPermissions }); req._secret = secret; diff --git a/backend/src/middleware/requireSecretsAuth.ts b/backend/src/middleware/requireSecretsAuth.ts index 5a550156a..a076a3f1a 100644 --- a/backend/src/middleware/requireSecretsAuth.ts +++ b/backend/src/middleware/requireSecretsAuth.ts @@ -1,4 +1,5 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { UnauthorizedRequestError } from '../utils/errors'; import { Secret, Membership } from '../models'; import { validateClientForSecrets } from '../helpers/secrets'; @@ -24,7 +25,7 @@ const requireSecretsAuth = ({ req.secrets = await validateClientForSecrets({ authData: req.authData, - secretIds: [req.body.secretIds], + secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), requiredPermissions }); diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index ff9764ced..f2c825ba8 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -10,7 +10,9 @@ import { ADMIN, MEMBER, AUTH_MODE_JWT, - AUTH_MODE_SERVICE_TOKEN + AUTH_MODE_SERVICE_TOKEN, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS } from '../../variables'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret'; import { secretController } from '../../controllers/v2'; @@ -75,7 +77,8 @@ router.get( acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS] }), validateRequest, secretController.getSecret @@ -103,7 +106,8 @@ router.delete( acceptedAuthModes: [AUTH_MODE_JWT] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] }), param('secretId').isMongoId(), validateRequest, diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 792491557..57657b079 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -1,5 +1,6 @@ import express from 'express'; const router = express.Router(); +import { Types } from 'mongoose'; import { requireAuth, requireWorkspaceAuth, @@ -47,7 +48,7 @@ router.post( if (secretIds.length > 0) { req.secrets = await validateClientForSecrets({ authData: req.authData, - secretIds, + secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), requiredPermissions: [] }); }