Update middleware for requireSecretAuth

This commit is contained in:
Tuan Dang
2023-04-12 14:17:20 +03:00
parent c768383f7e
commit e2139882da
8 changed files with 175 additions and 35 deletions

View File

@@ -7,7 +7,12 @@ import {
} from '../../../middleware'; } from '../../../middleware';
import { query, param, body } from 'express-validator'; import { query, param, body } from 'express-validator';
import { secretController } from '../../controllers/v1'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../../variables'; import {
ADMIN,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../../variables';
router.get( router.get(
'/:secretId/secret-versions', '/:secretId/secret-versions',
@@ -15,7 +20,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
param('secretId').exists().trim(), param('secretId').exists().trim(),
query('offset').exists().isInt(), query('offset').exists().isInt(),
@@ -30,7 +36,8 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}), }),
param('secretId').exists().trim(), param('secretId').exists().trim(),
body('version').exists().isInt(), body('version').exists().isInt(),

View File

@@ -131,7 +131,7 @@ const validateMembership = async ({
if (acceptedRoles) { if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) { if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({ message: 'Failed to validate workspace membership role' }); throw BadRequestError({ message: 'Failed authorization for membership role' });
} }
} }

View File

@@ -10,15 +10,24 @@ import {
ISecret ISecret
} from '../models'; } from '../models';
import { import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForSecret,
validateUserClientForSecrets validateUserClientForSecrets
} from '../helpers/user'; } from '../helpers/user';
import { import {
validateServiceTokenDataClientForSecrets validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData'; } from '../helpers/serviceTokenData';
import { import {
validateServiceAccountClientForSecrets validateServiceAccountClientForSecrets,
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount'; } from '../helpers/serviceAccount';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError
} from '../utils/errors';
import { import {
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
@@ -27,12 +36,91 @@ import {
} from '../variables'; } 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 {Object} obj
* @param {User} obj.user - user client * @param {Object} obj.authData - authenticated client details
* @param {ServiceAccount} obj.serviceAccount - service account client * @param {Types.ObjectId} obj.secretId - id of secret to validate against
* @param {ServiceTokenData} obj.service - service token client * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.secretIds - ids of secrets to validate against * @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 ({ const validateClientForSecrets = async ({
authData, authData,
@@ -43,7 +131,7 @@ const validateClientForSecrets = async ({
authMode: string; authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData; authPayload: IUser | IServiceAccount | IServiceTokenData;
}, },
secretIds: string[]; secretIds: Types.ObjectId[];
requiredPermissions: string[]; requiredPermissions: string[];
}) => { }) => {
@@ -51,7 +139,7 @@ const validateClientForSecrets = async ({
secrets = await Secret.find({ secrets = await Secret.find({
_id: { _id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) $in: secretIds
} }
}); });
@@ -105,5 +193,6 @@ const validateClientForSecrets = async ({
} }
export { export {
validateClientForSecret,
validateClientForSecrets validateClientForSecrets
} }

View File

@@ -219,6 +219,42 @@ const validateUserClientForWorkspace = async ({
return membership; 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] * Validate that user (client) can access secrets [secrets]
* with required permissions [requiredPermissions] * with required permissions [requiredPermissions]
@@ -236,7 +272,8 @@ const validateUserClientForWorkspace = async ({
secrets: ISecret[]; secrets: ISecret[];
requiredPermissions?: string[]; requiredPermissions?: string[];
}) => { }) => {
// TODO: refactor
// TODO: add acceptedRoles?
const userMemberships = await Membership.find({ user: user._id }) const userMemberships = await Membership.find({ user: user._id })
const userMembershipById = _.keyBy(userMemberships, 'workspace'); const userMembershipById = _.keyBy(userMemberships, 'workspace');
@@ -326,5 +363,6 @@ export {
validateUserClientForWorkspace, validateUserClientForWorkspace,
validateUserClientForSecrets, validateUserClientForSecrets,
validateUserClientForServiceAccount, validateUserClientForServiceAccount,
validateUserClientForOrganization validateUserClientForOrganization,
validateUserClientForSecret
}; };

View File

@@ -1,12 +1,17 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors';
import { Secret } from '../models'; import { Secret } from '../models';
import { import {
validateMembership validateMembership
} from '../helpers/membership'; } from '../helpers/membership';
import {
validateClientForSecret
} from '../helpers/secrets';
// note: used for old /v1/secret and /v2/secret routes. // 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. * 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 * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/ */
const requireSecretAuth = ({ const requireSecretAuth = ({
acceptedRoles acceptedRoles,
requiredPermissions
}: { }: {
acceptedRoles: Array<'admin' | 'member'>; acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const { secretId } = req.params; const { secretId } = req.params;
const secret = await Secret.findById(secretId); const secret = await validateClientForSecret({
authData: req.authData,
if (!secret) { secretId: new Types.ObjectId(secretId),
return next(SecretNotFoundError({ acceptedRoles,
message: 'Failed to find secret' requiredPermissions
}));
}
await validateMembership({
userId: req.user._id,
workspaceId: secret.workspace,
acceptedRoles
}); });
req._secret = secret; req._secret = secret;

View File

@@ -1,4 +1,5 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models'; import { Secret, Membership } from '../models';
import { validateClientForSecrets } from '../helpers/secrets'; import { validateClientForSecrets } from '../helpers/secrets';
@@ -24,7 +25,7 @@ const requireSecretsAuth = ({
req.secrets = await validateClientForSecrets({ req.secrets = await validateClientForSecrets({
authData: req.authData, authData: req.authData,
secretIds: [req.body.secretIds], secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions requiredPermissions
}); });

View File

@@ -10,7 +10,9 @@ import {
ADMIN, ADMIN,
MEMBER, MEMBER,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_TOKEN AUTH_MODE_SERVICE_TOKEN,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../variables'; } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2'; import { secretController } from '../../controllers/v2';
@@ -75,7 +77,8 @@ router.get(
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
validateRequest, validateRequest,
secretController.getSecret secretController.getSecret
@@ -103,7 +106,8 @@ router.delete(
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}), }),
param('secretId').isMongoId(), param('secretId').isMongoId(),
validateRequest, validateRequest,

View File

@@ -1,5 +1,6 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { Types } from 'mongoose';
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
@@ -47,7 +48,7 @@ router.post(
if (secretIds.length > 0) { if (secretIds.length > 0) {
req.secrets = await validateClientForSecrets({ req.secrets = await validateClientForSecrets({
authData: req.authData, authData: req.authData,
secretIds, secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
requiredPermissions: [] requiredPermissions: []
}); });
} }