diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index b82dca9fe..4985420fb 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -11,7 +11,7 @@ import { import { SecretVersion } from '../../ee/models'; import { BadRequestError } from '../../utils/errors'; import _ from 'lodash'; -import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; /** * Create new workspace environment named [environmentName] under workspace with id @@ -244,8 +244,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async ( throw BadRequestError() } relatedWorkspace.environments.forEach(environment => { - const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) - const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) + const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS }) + const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS }) if (isReadBlocked && isWriteBlocked) { return } else { diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 169a8a8cb..82b5c65d6 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -17,7 +17,7 @@ import { eventPushSecrets } from '../../events'; import { EESecretService, EELogService } from '../../ee/services'; import { TelemetryService } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; -import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; +import { PERMISSION_WRITE_SECRETS } from '../../variables'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import Tag from '../../models/tag'; import _, { eq } from 'lodash'; @@ -336,7 +336,7 @@ export const createSecrets = async (req: Request, res: Response) => { const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body; if (req.user) { - const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, ABILITY_WRITE) + const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, PERMISSION_WRITE_SECRETS) if (!hasAccess) { throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } diff --git a/backend/src/controllers/v2/serviceAccountsController.ts b/backend/src/controllers/v2/serviceAccountsController.ts index 822dbccaa..38da62bc8 100644 --- a/backend/src/controllers/v2/serviceAccountsController.ts +++ b/backend/src/controllers/v2/serviceAccountsController.ts @@ -14,12 +14,29 @@ import { import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors'; import { getSaltRounds } from '../../config'; +/** + * Return service account tied to the request (service account) client + * @param req + * @param res + */ +export const getCurrentServiceAccount = async (req: Request, res: Response) => { + const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id); + + if (!serviceAccount) { + throw ServiceAccountNotFoundError({ message: 'Failed to find service account' }); + } + + return res.status(200).send({ + serviceAccount + }); +} + /** * Return service account with id [serviceAccountId] * @param req * @param res */ -export const getServiceAccount = async (req: Request, res: Response) => { +export const getServiceAccountById = async (req: Request, res: Response) => { const { serviceAccountId } = req.params; const serviceAccount = await ServiceAccount.findById(serviceAccountId); @@ -136,23 +153,6 @@ export const addServiceAccountKey = async (req: Request, res: Response) => { return serviceAccountKey; } -// /** -// * Return organization-level permissions for service account with id [serviceAccountId] -// * @param req -// * @param res -// */ -// export const getServiceAccountOrganizationPermissions = async (req: Request, res: Response) => { -// const { serviceAccountId } = req.params; - -// const permissions = await ServiceAccountOrganizationPermissions.findOne({ -// serviceAccount: new Types.ObjectId(serviceAccountId), -// }); - -// return res.status(200).send({ -// permissions -// }); -// } - /** * Return workspace-level permission for service account with id [serviceAccountId] * @param req @@ -168,23 +168,6 @@ export const getServiceAccountWorkspacePermissions = async (req: Request, res: R }); } -// /** -// * Add organization permissions to service account with id [serviceAccountId] -// * @param req -// * @param res -// */ -// export const addServiceAccountOrganizationPermission = async (req: Request, res: Response) => { -// const permissions = ServiceAccountOrganizationPermissions.findOne({ -// serviceAccount: req.serviceAccount._id -// }); - -// // TODO - -// return res.status(200).send({ -// permissions -// }); -// } - /** * Add a workspace permission to service account with id [serviceAccountId] * @param req @@ -300,4 +283,23 @@ export const deleteServiceAccount = async (req: Request, res: Response) => { return res.status(200).send({ serviceAccount }); +} + +/** + * Return service account keys for service account with id [serviceAccountId] + * @param req + * @param res + * @returns + */ +export const getServiceAccountKeys = async (req: Request, res: Response) => { + const workspaceId = req.query.workspaceId as string; + + const serviceAccountKeys = await ServiceAccountKey.find({ + serviceAccount: req.serviceAccount._id, + ...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}) + }); + + return res.status(200).send({ + serviceAccountKeys + }); } \ No newline at end of file diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index a4e06f8e4..4b42bd373 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -3,10 +3,16 @@ import { Request, Response } from 'express'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; import { + User, + ServiceAccount, ServiceTokenData } from '../../models'; import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; -import { ABILITY_READ } from '../../variables/organization'; +import { + PERMISSION_READ_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT +} from '../../variables'; import { getSaltRounds } from '../../config'; /** @@ -53,59 +59,57 @@ export const getServiceTokenData = async (req: Request, res: Response) => { * @returns */ export const createServiceTokenData = async (req: Request, res: Response) => { - let serviceToken, serviceTokenData; + let serviceTokenData; - try { - const { - name, - workspaceId, - environment, - encryptedKey, - iv, - tag, - expiresIn, - permissions - } = req.body; + const { + name, + workspaceId, + environment, + encryptedKey, + iv, + tag, + expiresIn, + permissions + } = req.body; - const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ) - if (!hasAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, getSaltRounds()); - const secret = crypto.randomBytes(16).toString('hex'); - const secretHash = await bcrypt.hash(secret, getSaltRounds()); + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - - serviceTokenData = await new ServiceTokenData({ - name, - workspace: workspaceId, - environment, - user: req.user._id, - expiresAt, - secretHash, - encryptedKey, - iv, - tag, - permissions - }).save(); - - // return service token data without sensitive data - serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); - - if (!serviceTokenData) throw new Error('Failed to find service token data'); - - serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create service token data' - }); + let user, serviceAccount; + + if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { + user = req.authData.authPayload._id; } + if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) { + serviceAccount = req.authData.authPayload._id; + } + + serviceTokenData = await new ServiceTokenData({ + name, + workspace: workspaceId, + environment, + user, + serviceAccount, + expiresAt, + secretHash, + encryptedKey, + iv, + tag, + permissions + }).save(); + + + // return service token data without sensitive data + serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); + + if (!serviceTokenData) throw new Error('Failed to find service token data'); + + const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + return res.status(200).send({ serviceToken, serviceTokenData diff --git a/backend/src/ee/controllers/v1/membershipController.ts b/backend/src/ee/controllers/v1/membershipController.ts index fcf158ba8..35534d19c 100644 --- a/backend/src/ee/controllers/v1/membershipController.ts +++ b/backend/src/ee/controllers/v1/membershipController.ts @@ -2,7 +2,8 @@ import { Request, Response } from "express"; import { Membership, Workspace } from "../../../models"; import { IMembershipPermission } from "../../../models/membership"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; -import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization"; +import { ADMIN, MEMBER } from "../../../variables/organization"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables'; import { Builder } from "builder-pattern" import _ from "lodash"; @@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) => const { membershipId } = req.params; const { permissions } = req.body; const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => { - if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) { + if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) { throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" }) } diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts index d965c31e0..c97a51619 100644 --- a/backend/src/ee/helpers/checkMembershipPermissions.ts +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -1,7 +1,7 @@ import { Types } from 'mongoose'; import _ from "lodash"; import { Membership } from "../../models"; -import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) @@ -26,8 +26,8 @@ export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceI } const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; - const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); - const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); + const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); // case: you have write only if read is blocked and write is not if (isReadDisallowed && !isWriteDisallowed) { @@ -44,8 +44,8 @@ export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Type } const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; - const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); - const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); + const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); if (isReadBlocked && isWriteDisallowed) { return true diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 57fa6ebda..7a7979357 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -21,7 +21,6 @@ import { ACTION_READ_SECRETS } from '../variables'; import _ from 'lodash'; -import { ABILITY_WRITE } from '../variables/organization'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; interface V1PushSecret { diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 11f19499e..09418560d 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -18,7 +18,7 @@ import { import { validateServiceAccountClientForSecrets } from '../helpers/serviceAccount'; -import { BadRequestError } from '../utils/errors'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, @@ -60,21 +60,23 @@ const validateClientForSecrets = async ({ } if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - // TODO await validateUserClientForSecrets({ user: authData.authPayload, secrets, requiredPermissions }); + + return secrets; } if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - // TODO await validateServiceAccountClientForSecrets({ serviceAccount: authData.authPayload, secrets, requiredPermissions }); + + return secrets; } if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { @@ -83,18 +85,23 @@ const validateClientForSecrets = async ({ secrets, requiredPermissions }); + + return secrets; } if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - // TODO await validateUserClientForSecrets({ user: authData.authPayload, secrets, requiredPermissions }); + + return secrets; } - - return secrets; + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for secrets resource' + }); } export { diff --git a/backend/src/helpers/serviceAccount.ts b/backend/src/helpers/serviceAccount.ts index fca64e8fd..f0fe3063e 100644 --- a/backend/src/helpers/serviceAccount.ts +++ b/backend/src/helpers/serviceAccount.ts @@ -1,17 +1,102 @@ import _ from 'lodash'; import { Types } from 'mongoose'; import { + User, + IUser, + ServiceAccount, IServiceAccount, + ServiceTokenData, + IServiceTokenData, ISecret, ServiceAccountWorkspacePermission } from '../models'; +import { + BadRequestError, + UnauthorizedRequestError, + ServiceAccountNotFoundError +} from '../utils/errors'; +import { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; +import { + validateUserClientForServiceAccount +} from '../helpers/user'; + +const validateClientForServiceAccount = async ({ + authData, + serviceAccountId, + requiredPermissions +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, + serviceAccountId: Types.ObjectId; + requiredPermissions?: string[]; +}) => { + const serviceAccount = await ServiceAccount.findById(serviceAccountId); + + if (!serviceAccount) { + throw ServiceAccountNotFoundError({ + message: 'Failed to find service account' + }); + } + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForServiceAccount({ + user: authData.authPayload, + serviceAccount, + requiredPermissions + }); + + return serviceAccount; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForServiceAccount({ + serviceAccount: authData.authPayload, + targetServiceAccount: serviceAccount, + requiredPermissions + }); + + return serviceAccount; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for service account resource' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForServiceAccount({ + user: authData.authPayload, + serviceAccount, + requiredPermissions + }); + + return serviceAccount; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for service account resource' + }); +} /** - * Validate that serviceAccount (client) can access workspace + * Validate that service account (client) can access workspace * with id [workspaceId] and its environment [environment] with required permissions * [requiredPermissions] - * @param {Object} obj - * @param {Object} obj. + * @param {Object} obj + * @param {ServiceAccount} obj.serviceAccount - service account client + * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against + * @param {String} environment - (optional) environment in workspace to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ const validateServiceAccountClientForWorkspace = async ({ serviceAccount, @@ -21,18 +106,51 @@ import { }: { serviceAccount: IServiceAccount; workspaceId: Types.ObjectId; - environment: string; - requiredPermissions: string[]; + environment?: string; + requiredPermissions?: string[]; }) => { - // TODO - return []; + // TODO: add service account API support for workspace-level endpoints that are not + // tied to any specific environment + + if (environment) { + const permission = await ServiceAccountWorkspacePermission.findOne({ + serviceAccount, + workspace: new Types.ObjectId(workspaceId), + environment + }); + + if (!permission) throw UnauthorizedRequestError({ + message: 'Failed service account authorization for the given workspace environment' + }); + + // TODO: refactor + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + if (!permission.read) runningIsDisallowed = true; + break; + case PERMISSION_WRITE_SECRETS: + if (!permission.write) runningIsDisallowed = true; + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + } } /** * Validate that service account (client) can access secrets * with required permissions [requiredPermissions] * @param {Object} obj - * @param {ServiceTokenData} obj.serviceAccount - service account client + * @param {ServiceAccount} obj.serviceAccount - service account client * @param {Secret[]} secrets - secrets to validate against * @param {string[]} requiredPermissions - required permissions as part of the endpoint */ @@ -49,15 +167,73 @@ import { const permissions = await ServiceAccountWorkspacePermission.find({ serviceAccount: serviceAccount._id }); + const permissionsObj = _.keyBy(permissions, (p) => { return `${p.workspace.toString()}-${p.environment}` }); + secrets.forEach((secret: ISecret) => { + const permission = permissionsObj[`${secret.workspace.toString()}-${secret.environment}`]; + + if (!permission) throw BadRequestError({ + message: 'Failed to find any permission for the secret workspace and environment' + }); + + requiredPermissions?.forEach((requiredPermission: string) => { + // TODO: refactor + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + if (!permission.read) runningIsDisallowed = true; + break; + case PERMISSION_WRITE_SECRETS: + if (!permission.write) runningIsDisallowed = true; + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + }); + }); + // TODO return []; } +/** + * Validate that service account (client) can access target service + * account [serviceAccount] with required permissions [requiredPermissions] + * @param {Object} obj + * @param {SerivceAccount} obj.serviceAccount - service account client + * @param {ServiceAccount} targetServiceAccount - target service account to validate against + * @param {string[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateServiceAccountClientForServiceAccount = ({ + serviceAccount, + targetServiceAccount, + requiredPermissions +}: { + serviceAccount: IServiceAccount; + targetServiceAccount: IServiceAccount; + requiredPermissions?: string[]; +}) => { + if (!serviceAccount.organization.equals(targetServiceAccount.organization)) { + throw UnauthorizedRequestError({ + message: 'Failed service account authorization for the given service account' + }); + } +} + export { + validateClientForServiceAccount, validateServiceAccountClientForWorkspace, - validateServiceAccountClientForSecrets + validateServiceAccountClientForSecrets, + validateServiceAccountClientForServiceAccount } \ No newline at end of file diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 668635f91..ddf313da9 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -3,6 +3,7 @@ import { Types } from 'mongoose'; import { IUser, ISecret, + IServiceAccount, User, Membership } from '../models'; @@ -11,8 +12,12 @@ import { validateMembership } from './membership'; import _ from 'lodash'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { - ABILITY_WRITE -} from '../variables/organization'; + validateMembershipOrg +} from '../helpers/membershipOrg'; +import { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from '../variables'; /** * Initialize a user under email [email] @@ -180,24 +185,38 @@ const validateUserClientForWorkspace = async ({ requiredPermissions?: string[]; }) => { - // org-level and workspace-level permissions? - workspace-level env scoped? - // validate user membership in workspace const membership = await validateMembership({ userId: user._id, workspaceId }); - // validate user permission - - - // TODO: validate that user can perform action on environment in workspace + // TODO: refactor + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); + break; + case PERMISSION_WRITE_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); return membership; } /** - * Validate that user (client) can access secrets with ids [secretIds] + * Validate that user (client) can access secrets [secrets] * with required permissions [requiredPermissions] * @param {Object} obj * @param {User} obj.user - user client @@ -213,7 +232,7 @@ const validateUserClientForWorkspace = async ({ secrets: ISecret[]; requiredPermissions?: string[]; }) => { - // TODO: consider refactor + // TODO: refactor const userMemberships = await Membership.find({ user: user._id }) const userMembershipById = _.keyBy(userMemberships, 'workspace'); @@ -221,27 +240,59 @@ const validateUserClientForWorkspace = async ({ // for each secret check if the secret belongs to a workspace the user is a member of secrets.forEach((secret: ISecret) => { - if (workspaceIdsSet.has(secret.workspace.toString())) { + if (!workspaceIdsSet.has(secret.workspace.toString())) { + throw BadRequestError({ + message: 'Failed authorization for the secret' + }); + } + + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; - const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE }); + const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); if (isDisallowed) { throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' }); } - } else { - throw BadRequestError({ - message: 'You cannot edit secrets of a workspace you are not a member of' - }); } }); } +/** + * Validate that user (client) can access service account [serviceAccount] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {ServiceAccount} obj.serviceAccount - service account to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateUserClientForServiceAccount = async ({ + user, + serviceAccount, + requiredPermissions +}: { + user: IUser; + serviceAccount: IServiceAccount; + requiredPermissions?: string[]; +}) => { + if (!serviceAccount.user.equals(user._id)) { + // case: user who created service account is not the + // same user that is on the request + await validateMembershipOrg({ + userId: user._id, + organizationId: serviceAccount.organization, + acceptedRoles: [], + acceptedStatuses: [] + }); + } +} + export { setupAccount, completeAccount, checkUserDevice, validateUserClientForWorkspace, - validateUserClientForSecrets + validateUserClientForSecrets, + validateUserClientForServiceAccount }; diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 0dd471004..97851df33 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -53,30 +53,26 @@ const validateClientForWorkspace = async ({ requiredPermissions?: string[]; }) => { - let membership; if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - membership = await validateUserClientForWorkspace({ + const membership = await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId, environment, requiredPermissions }); - // TODO: validate user against [requiredPermissions] + return ({ membership }); } if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - const permission = await ServiceAccountWorkspacePermission.findOne({ - serviceAccount: authData.authPayload._id, - workspace: new Types.ObjectId(workspaceId), - environment + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId, + environment, + requiredPermissions }); - if (!permission) throw UnauthorizedRequestError({ - message: 'Failed service account authorization for the given workspace environment' - }); - - // TODO: validate [requiredPermissions] against [permission] + return {}; } if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { @@ -87,20 +83,22 @@ const validateClientForWorkspace = async ({ requiredPermissions }); - // TODO: validate [requiredPermissions] against [permission] + return {}; } if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - membership = await validateUserClientForWorkspace({ + const membership = await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId, environment, requiredPermissions }); + + return ({ membership }); } - return ({ - membership + throw UnauthorizedRequestError({ + message: 'Failed client authorization for workspace resource' }); } diff --git a/backend/src/middleware/requireServiceAccountAuth.ts b/backend/src/middleware/requireServiceAccountAuth.ts index f5bdb8f3f..40861a737 100644 --- a/backend/src/middleware/requireServiceAccountAuth.ts +++ b/backend/src/middleware/requireServiceAccountAuth.ts @@ -1,4 +1,5 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { ServiceAccount } from '../models'; import { ServiceAccountNotFoundError @@ -6,38 +7,31 @@ import { import { validateMembershipOrg } from '../helpers/membershipOrg'; +import { + validateClientForServiceAccount +} from '../helpers/serviceAccount'; type req = 'params' | 'body' | 'query'; const requireServiceAccountAuth = ({ acceptedRoles, acceptedStatuses, - location = 'params' + locationServiceAccountId = 'params', + requiredPermissions = [] }: { acceptedRoles: string[]; acceptedStatuses: string[]; - location?: req; + locationServiceAccountId?: req; + requiredPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - const serviceAccountId = req[location].serviceAccountId; - const serviceAccount = await ServiceAccount.findById(serviceAccountId); + const serviceAccountId = req[locationServiceAccountId].serviceAccountId; - if (!serviceAccount) { - return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account' })); - } - - if (serviceAccount.user.toString() !== req.user.id.toString()) { - // case: creator of the service account is different from - // the user on the request -> apply middleware role/status validation - await validateMembershipOrg({ - userId: req.user._id, - organizationId: serviceAccount.organization, - acceptedRoles, - acceptedStatuses - }); - } - - req.serviceAccount = serviceAccount; + req.serviceAccount = await validateClientForServiceAccount({ + authData: req.authData, + serviceAccountId: new Types.ObjectId(serviceAccountId), + requiredPermissions + }); next(); } diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index 887e2e92f..deeb4eb28 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -6,6 +6,7 @@ export interface IServiceTokenData extends Document { workspace: Types.ObjectId; environment: string; user: Types.ObjectId; + serviceAccount: Types.ObjectId; expiresAt: Date; secretHash: string; encryptedKey: string; @@ -31,8 +32,11 @@ const serviceTokenDataSchema = new Schema( }, user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: 'User' + }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount' }, expiresAt: { type: Date diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 6449b845e..8ab9812b3 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -197,5 +197,4 @@ router.delete( secretsController.deleteSecrets ); -export default router; - +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/serviceAccounts.ts b/backend/src/routes/v2/serviceAccounts.ts index 397c98d12..9259af89d 100644 --- a/backend/src/routes/v2/serviceAccounts.ts +++ b/backend/src/routes/v2/serviceAccounts.ts @@ -17,6 +17,14 @@ import { } from '../../variables'; import { serviceAccountsController } from '../../controllers/v2'; +router.get( // TODO: check + '/me', + requireAuth({ + acceptedAuthModes: ['serviceAccount'] + }), + serviceAccountsController.getCurrentServiceAccount +); + router.get( '/:serviceAccountId', param('serviceAccountId').exists().isString().trim(), @@ -27,7 +35,7 @@ router.get( acceptedRoles: [OWNER, ADMIN], acceptedStatuses: [ACCEPTED] }), - serviceAccountsController.getServiceAccount + serviceAccountsController.getServiceAccountById ); router.post( @@ -76,22 +84,6 @@ router.delete( serviceAccountsController.deleteServiceAccount ); -// router.get( -// '/:serviceAccountId/permissions/organization', -// param('serviceAccountId').exists().isString().trim(), -// query('offset').exists(), -// query('limit').exists(), -// validateRequest, -// requireAuth({ -// acceptedAuthModes: ['jwt'] -// }), -// requireServiceAccountAuth({ -// acceptedRoles: [OWNER, ADMIN], -// acceptedStatuses: [ACCEPTED] -// }), -// serviceAccountsController.getServiceAccountOrganizationPermission -// ); - router.get( '/:serviceAccountId/permissions/workspace', param('serviceAccountId').exists().isString().trim(), @@ -106,20 +98,6 @@ router.get( serviceAccountsController.getServiceAccountWorkspacePermissions ); -// router.post( -// '/:serviceAccountId/permissions/organization', -// param('serviceAccountId').exists().isString().trim(), -// validateRequest, -// requireAuth({ -// acceptedAuthModes: ['jwt'] -// }), -// requireServiceAccountAuth({ -// acceptedRoles: [OWNER, ADMIN], -// acceptedStatuses: [ACCEPTED] -// }), -// serviceAccountsController.addServiceAccountOrganizationPermission -// ); - router.post( '/:serviceAccountId/permissions/workspace', param('serviceAccountId').exists().isString().trim(), @@ -163,16 +141,17 @@ router.delete( serviceAccountsController.deleteServiceAccountWorkspacePermission ); -// router.post( -// '/:serviceAccountId/key', -// body('workspaceId').exists().isString().trim(), -// body('encryptedKey').exists().isString().trim(), -// body('nonce').exists().isString().trim(), -// requireServiceAccountAuth({ -// acceptedRoles: [OWNER, ADMIN, MEMBER], -// acceptedStatuses: [ACCEPTED] -// }), -// serviceAccountsController.addServiceAccountKey -// ); +router.get( + '/:serviceAccountId/keys', + query('workspaceId').optional().isString(), + requireAuth({ + acceptedAuthModes: ['jwt', 'serviceAccount'] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + serviceAccountsController.getServiceAccountKeys +); export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 54db35724..8d65e94e8 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -10,6 +10,7 @@ import { param, body } from 'express-validator'; import { ADMIN, MEMBER, + PERMISSION_WRITE_SECRETS } from '../../variables'; import { serviceTokenDataController } from '../../controllers/v2'; @@ -24,11 +25,13 @@ router.get( router.post( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'serviceAccount'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), body('name').exists().isString().trim(), body('workspaceId').exists().isString().trim(), diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 91af2ff25..80c7102c1 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -6,11 +6,7 @@ const MEMBER = "member"; // membership statuses const INVITED = "invited"; -// membership permissions ability -const ABILITY_READ = "read"; -const ABILITY_WRITE = "write"; - // -- organization const ACCEPTED = "accepted"; -export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE }; +export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED }; diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx index 9ce8ab406..3e3da9fd7 100644 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx +++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx @@ -117,7 +117,7 @@ export const SAProjectLevelPermissionsTable = ({ const { ciphertext, nonce } = encryptAssymmetric({ plaintext: key, publicKey: serviceAccount.publicKey, - privateKey: PRIVATE_KEY + privateKey }); await createServiceAccountProjectLevelPermission.mutateAsync({ @@ -171,8 +171,6 @@ export const SAProjectLevelPermissionsTable = ({ Environment Read Write - Update - Delete