From a940fa210a7b34a8c26b9c47e1dee4bfcdafa220 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 29 Jan 2023 21:04:47 -0800 Subject: [PATCH] Add deny api/get envs api --- backend/package-lock.json | 25 ++++ backend/package.json | 3 + .../controllers/v1/membershipController.ts | 4 +- .../controllers/v2/environmentController.ts | 53 +++++++++ .../src/controllers/v2/secretsController.ts | 22 +++- backend/src/ee/controllers/v1/index.ts | 4 +- .../ee/controllers/v1/membershipController.ts | 63 +++++++++++ .../ee/helpers/checkMembershipPermissions.ts | 18 +++ backend/src/helpers/secret.ts | 107 ++++++++++-------- backend/src/models/membership.ts | 20 +++- backend/src/rbac/main.polar | 15 --- backend/src/routes/v1/membership.ts | 24 +++- backend/src/routes/v2/environment.ts | 13 +++ backend/src/variables/organization.ts | 8 +- 14 files changed, 303 insertions(+), 76 deletions(-) create mode 100644 backend/src/ee/controllers/v1/membershipController.ts create mode 100644 backend/src/ee/helpers/checkMembershipPermissions.ts delete mode 100644 backend/src/rbac/main.polar diff --git a/backend/package-lock.json b/backend/package-lock.json index 9caf23dd1..2c7abb793 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "axios": "^1.1.3", "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", + "builder-pattern": "^2.2.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "crypto-js": "^4.1.1", @@ -32,6 +33,7 @@ "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.2.2", @@ -58,6 +60,7 @@ "@types/express": "^4.17.14", "@types/jest": "^29.2.4", "@types/jsonwebtoken": "^8.5.9", + "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/supertest": "^2.0.12", @@ -3221,6 +3224,12 @@ "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" }, + "node_modules/@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "dev": true + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -4085,6 +4094,11 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/builder-pattern": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/builder-pattern/-/builder-pattern-2.2.0.tgz", + "integrity": "sha512-cES3qdeBzA4QyJi7rV/l/kAhIFX6AKo3vK66ZPXLNpjcQWCS8sjLKscly8imlfW2YPTo/hquMRMnaWpZ80Kj+g==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -14641,6 +14655,12 @@ "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" }, + "@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "dev": true + }, "@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -15269,6 +15289,11 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "builder-pattern": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/builder-pattern/-/builder-pattern-2.2.0.tgz", + "integrity": "sha512-cES3qdeBzA4QyJi7rV/l/kAhIFX6AKo3vK66ZPXLNpjcQWCS8sjLKscly8imlfW2YPTo/hquMRMnaWpZ80Kj+g==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index 7a1552dd3..e64d9d446 100644 --- a/backend/package.json +++ b/backend/package.json @@ -36,6 +36,7 @@ "@types/express": "^4.17.14", "@types/jest": "^29.2.4", "@types/jsonwebtoken": "^8.5.9", + "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/supertest": "^2.0.12", @@ -85,6 +86,7 @@ "axios": "^1.1.3", "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", + "builder-pattern": "^2.2.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "crypto-js": "^4.1.1", @@ -98,6 +100,7 @@ "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.2.2", diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 42cc29d9c..3627ce3fb 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Membership, MembershipOrg, User, Key } from '../../models'; +import { Membership, MembershipOrg, User, Key, IMembership, Workspace } from '../../models'; import { findMembership, deleteMembership as deleteMember @@ -230,4 +230,4 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { invitee, latestKey }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 47832f641..d92ec8264 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -6,8 +6,12 @@ import { Workspace, Integration, ServiceTokenData, + Membership, } from '../../models'; import { SecretVersion } from '../../ee/models'; +import { BadRequestError } from '../../utils/errors'; +import _ from 'lodash'; +import { ABILITY_READ } from '../../variables/organization'; /** * Create new workspace environment named [environmentName] under workspace with id @@ -120,6 +124,15 @@ export const renameWorkspaceEnvironment = async ( { workspace: workspaceId, environment: oldEnvironmentSlug }, { environment: environmentSlug } ); + await Membership.updateMany( + { + workspace: workspaceId, + "deniedPermissions.environmentSlug": oldEnvironmentSlug + }, + { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, + { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } + ) + } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -188,6 +201,11 @@ export const deleteWorkspaceEnvironment = async ( workspace: workspaceId, environment: environmentSlug, }); + await Membership.updateMany( + { workspace: workspaceId }, + { $pull: { deniedPermissions: { environmentSlug: environmentSlug } } } + ) + } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -202,3 +220,38 @@ export const deleteWorkspaceEnvironment = async ( environment: environmentSlug, }); }; + + +export const getAllAccessibleEnvironmentsOfWorkspace = async ( + req: Request, + res: Response +) => { + const { workspaceId } = req.params; + const workspacesUserIsMemberOf = await Membership.findOne({ + workspace: workspaceId, + user: req.user + }) + + if (!workspacesUserIsMemberOf) { + throw BadRequestError() + } + + const accessibleEnvironments: { name: string; slug: string; }[] = [] + const deniedPermission = workspacesUserIsMemberOf.deniedPermissions + + const relatedWorkspace = await Workspace.findById(workspaceId) + if (!relatedWorkspace) { + 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 }) + if (isReadBlocked) { + return + } else { + accessibleEnvironments.push(environment) + } + }) + + res.json({ accessibleEnvironments }) +}; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 4a5a919fd..90eb8e48d 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,7 +1,7 @@ import to from 'await-to-js'; import { Types } from 'mongoose'; import { Request, Response } from 'express'; -import { ISecret, Secret } from '../../models'; +import { ISecret, Membership, Secret, Workspace } from '../../models'; import { SECRET_PERSONAL, SECRET_SHARED, @@ -10,13 +10,14 @@ import { ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS } from '../../variables'; -import { ValidationError } from '../../utils/errors'; +import { UnauthorizedRequestError, ValidationError } from '../../utils/errors'; import { EventService } from '../../services'; import { eventPushSecrets } from '../../events'; import { EESecretService, EELogService } from '../../ee/services'; import { postHogClient } from '../../services'; -import { BadRequestError } from '../../utils/errors'; import { getChannelFromUserAgent } from '../../utils/posthog'; +import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; +import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; /** * Create secret(s) for workspace with id [workspaceId] and environment [environment] @@ -76,9 +77,15 @@ export const createSecrets = async (req: Request, res: Response) => { } } */ + const channel = getChannelFromUserAgent(req.headers['user-agent']) const { workspaceId, environment } = req.body; + const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE) + if (!hasAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } + let toAdd; if (Array.isArray(req.body.secrets)) { // case: create multiple secrets @@ -269,6 +276,14 @@ export const getSecrets = async (req: Request, res: Response) => { userEmail = req.serviceTokenData.user.email; } + // none service token case as service tokens are already scoped + if (!req.serviceTokenData) { + const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) + if (!hasAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } + } + const [err, secrets] = await to(Secret.find( { workspace: workspaceId, @@ -371,7 +386,6 @@ export const updateSecrets = async (req: Request, res: Response) => { */ const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; - // TODO: move type interface PatchSecret { id: string; diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index dd88f1178..aaa697286 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -3,11 +3,13 @@ import * as secretController from './secretController'; import * as secretSnapshotController from './secretSnapshotController'; import * as workspaceController from './workspaceController'; import * as actionController from './actionController'; +import * as membershipController from './membershipController'; export { stripeController, secretController, secretSnapshotController, workspaceController, - actionController + actionController, + membershipController } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/membershipController.ts b/backend/src/ee/controllers/v1/membershipController.ts new file mode 100644 index 000000000..fcf158ba8 --- /dev/null +++ b/backend/src/ee/controllers/v1/membershipController.ts @@ -0,0 +1,63 @@ +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 { Builder } from "builder-pattern" +import _ from "lodash"; + +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)) { + throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" }) + } + + return Builder() + .environmentSlug(permission.environmentSlug) + .ability(permission.ability) + .build(); + }) + + const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual) + + const membershipToModify = await Membership.findById(membershipId) + if (!membershipToModify) { + throw BadRequestError({ message: "Unable to locate resource" }) + } + + // check if the user making the request is a admin of this project + if (![ADMIN, MEMBER].includes(membershipToModify.role)) { + throw UnauthorizedRequestError() + } + + // check if the requested slugs are indeed a part of this related workspace + const relatedWorkspace = await Workspace.findById(membershipToModify.workspace) + if (!relatedWorkspace) { + throw BadRequestError({ message: "Something went wrong when locating the related workspace" }) + } + + const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, 'slug'))); + + sanitizedMembershipPermissionsUnique.forEach(permission => { + if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) { + throw BadRequestError({ message: "Unknown environment slug reference" }) + } + }) + + // update the permissions + const updatedMembershipWithPermissions = await Membership.findByIdAndUpdate( + { _id: membershipToModify._id }, + { $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } }, + { new: true } + ) + + if (!updatedMembershipWithPermissions) { + throw BadRequestError({ message: "The resource has been removed before it can be modified" }) + } + + res.send({ + permissionsDenied: updatedMembershipWithPermissions.deniedPermissions + }) +} diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts new file mode 100644 index 000000000..55155e885 --- /dev/null +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -0,0 +1,18 @@ +import _ from "lodash"; +import { Membership } from "../../models"; + +export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { + const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) + if (!membershipForWorkspace) { + return false + } + + const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; + const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: action }); + + if (isDisallowed) { + return false + } + + return true +} \ No newline at end of file diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 74ba062bd..a16d114c6 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -12,14 +12,17 @@ import { import { IAction } from '../ee/models'; -import { - SECRET_SHARED, +import { + SECRET_SHARED, SECRET_PERSONAL, ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, ACTION_READ_SECRETS } from '../variables'; +import _ from 'lodash'; +import { ABILITY_WRITE } from '../variables/organization'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; /** * Validate that user with id [userId] can modify secrets with ids [secretIds] @@ -34,7 +37,7 @@ const validateSecrets = async ({ }: { userId: string; secretIds: string[]; -}) =>{ +}) => { let secrets; try { secrets = await Secret.find({ @@ -42,20 +45,31 @@ const validateSecrets = async ({ $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) } }); - - const workspaceIdsSet = new Set((await Membership.find({ - user: userId - }, 'workspace')) - .map((m) => m.workspace.toString())); - + + if (secrets.length != secretIds.length) { + throw BadRequestError({ message: 'Unable to validate some secrets' }) + } + + const userMemberships = await Membership.find({ user: userId }) + const userMembershipById = _.keyBy(userMemberships, 'workspace'); + const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); + + // 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())) { - throw new Error('Failed to validate secret'); + if (workspaceIdsSet.has(secret.workspace.toString())) { + const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; + const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE }); + + 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' }); } }); - + } catch (err) { - throw new Error('Failed to validate secrets'); + throw BadRequestError({ message: 'Unable to validate secrets' }) } return secrets; @@ -127,13 +141,13 @@ const v1PushSecrets = async ({ workspaceId, environment }); - - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) - , {}); - const newSecretsObj: any = secrets.reduce((accumulator, s) => + , {}); + const newSecretsObj: any = secrets.reduce((accumulator, s) => ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }) - , {}); + , {}); // handle deleting secrets const toDelete = oldSecrets @@ -150,12 +164,12 @@ const v1PushSecrets = async ({ secretIds: toDelete }); } - + const toUpdate = oldSecrets .filter((s) => { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { - if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue - || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) { + if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue + || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) { // case: filter secrets where value or comment changed return true; } @@ -165,7 +179,7 @@ const v1PushSecrets = async ({ return true; } } - + return false; }); @@ -217,7 +231,7 @@ const v1PushSecrets = async ({ }; }); await Secret.bulkWrite(operations as any); - + // (EE) add secret versions for updated secrets await EESecretService.addSecretVersions({ secretVersions: toUpdate.map(({ @@ -245,7 +259,7 @@ const v1PushSecrets = async ({ secretValueTag: newSecret.tagValue, secretValueHash: newSecret.hashValue }) - }) + }) }); // handle adding new secrets @@ -319,7 +333,7 @@ const v1PushSecrets = async ({ })) }); } - + // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId @@ -344,7 +358,7 @@ const v1PushSecrets = async ({ * @param {String} obj.channel - channel (web/cli/auto) * @param {String} obj.ipAddress - ip address of request to push secrets */ - const v2PushSecrets = async ({ +const v2PushSecrets = async ({ userId, workspaceId, environment, @@ -362,20 +376,20 @@ const v1PushSecrets = async ({ // TODO: clean up function and fix up types try { const actions: IAction[] = []; - + // construct useful data structures const oldSecrets = await getSecrets({ userId, workspaceId, environment }); - - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) - , {}); - const newSecretsObj: any = secrets.reduce((accumulator, s) => + , {}); + const newSecretsObj: any = secrets.reduce((accumulator, s) => ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) - , {}); + , {}); // handle deleting secrets const toDelete = oldSecrets @@ -391,7 +405,7 @@ const v1PushSecrets = async ({ await EESecretService.markDeletedSecretVersions({ secretIds: toDelete }); - + const deleteAction = await EELogService.createActionSecret({ name: ACTION_DELETE_SECRETS, userId, @@ -401,12 +415,12 @@ const v1PushSecrets = async ({ deleteAction && actions.push(deleteAction); } - + const toUpdate = oldSecrets .filter((s) => { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { - if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash - || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) { + if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash + || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) { // case: filter secrets where value or comment changed return true; } @@ -416,7 +430,7 @@ const v1PushSecrets = async ({ return true; } } - + return false; }); @@ -469,7 +483,7 @@ const v1PushSecrets = async ({ }; }); await Secret.bulkWrite(operations as any); - + // (EE) add secret versions for updated secrets await EESecretService.addSecretVersions({ secretVersions: toUpdate.map((s) => { @@ -482,7 +496,7 @@ const v1PushSecrets = async ({ environment: s.environment, isDeleted: false }) - }) + }) }); const updateAction = await EELogService.createActionSecret({ @@ -507,18 +521,19 @@ const v1PushSecrets = async ({ workspace: workspaceId, type: toAdd[idx].type, environment, - ...( toAdd[idx].type === 'personal' ? { user: userId } : {}) + ...(toAdd[idx].type === 'personal' ? { user: userId } : {}) })) ); // (EE) add secret versions for new secrets EESecretService.addSecretVersions({ - secretVersions: newSecrets.map((secretDocument) => { + secretVersions: newSecrets.map((secretDocument) => { return { ...secretDocument.toObject(), secret: secretDocument._id, isDeleted: false - }}) + } + }) }); const addAction = await EELogService.createActionSecret({ @@ -529,7 +544,7 @@ const v1PushSecrets = async ({ }); addAction && actions.push(addAction); } - + // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId @@ -560,7 +575,7 @@ const v1PushSecrets = async ({ * @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.environment - environment for secrets */ - const getSecrets = async ({ +const getSecrets = async ({ userId, workspaceId, environment @@ -570,7 +585,7 @@ const v1PushSecrets = async ({ environment: string; }): Promise => { let secrets: any; // TODO: FIX any - + try { // get shared workspace secrets const sharedSecrets = await Secret.find({ @@ -622,7 +637,7 @@ const pullSecrets = async ({ ipAddress: string; }): Promise => { let secrets: any; - + try { secrets = await getSecrets({ userId, diff --git a/backend/src/models/membership.ts b/backend/src/models/membership.ts index abbce82c1..55b8f2048 100644 --- a/backend/src/models/membership.ts +++ b/backend/src/models/membership.ts @@ -1,15 +1,21 @@ import { Schema, model, Types } from 'mongoose'; import { ADMIN, MEMBER } from '../variables'; +export interface IMembershipPermission { + environmentSlug: string, + ability: string +} + export interface IMembership { _id: Types.ObjectId; user: Types.ObjectId; inviteEmail?: string; workspace: Types.ObjectId; role: 'admin' | 'member'; + deniedPermissions: IMembershipPermission[] } -const membershipSchema = new Schema( +const membershipSchema = new Schema( { user: { type: Schema.Types.ObjectId, @@ -23,6 +29,18 @@ const membershipSchema = new Schema( ref: 'Workspace', required: true }, + deniedPermissions: { + type: [ + { + environmentSlug: String, + ability: { + type: String, + enum: ['read', 'write'] + }, + }, + ], + default: [] + }, role: { type: String, enum: [ADMIN, MEMBER], diff --git a/backend/src/rbac/main.polar b/backend/src/rbac/main.polar deleted file mode 100644 index c88143e51..000000000 --- a/backend/src/rbac/main.polar +++ /dev/null @@ -1,15 +0,0 @@ -# Actoers -actor User {} - -# Resource block -resource Organization { - permissions = ["view", "delete", "edit_billing", "edit_members"]; - roles = ["owner", "member"]; - - "view" if "member"; - "delete" if "owner"; - "edit_billing" if "owner"; - "edit_members" if "owner"; - - "member" if "owner"; -} \ No newline at end of file diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index 4481b8843..aaacada74 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -3,14 +3,15 @@ const router = express.Router(); import { body, param } from 'express-validator'; import { requireAuth, validateRequest } from '../../middleware'; import { membershipController } from '../../controllers/v1'; +import { membershipController as EEMembershipControllers } from '../../ee/controllers/v1'; // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) router.get( // used for old CLI (deprecate) '/:workspaceId/connect', requireAuth({ - acceptedAuthModes: ['jwt'] - }), + acceptedAuthModes: ['jwt'] + }), param('workspaceId').exists().trim(), validateRequest, membershipController.validateMembership @@ -19,8 +20,8 @@ router.get( // used for old CLI (deprecate) router.delete( '/:membershipId', requireAuth({ - acceptedAuthModes: ['jwt'] - }), + acceptedAuthModes: ['jwt'] + }), param('membershipId').exists().trim(), validateRequest, membershipController.deleteMembership @@ -29,11 +30,22 @@ router.delete( router.post( '/:membershipId/change-role', requireAuth({ - acceptedAuthModes: ['jwt'] - }), + acceptedAuthModes: ['jwt'] + }), body('role').exists().trim(), validateRequest, membershipController.changeMembershipRole ); +router.post( + '/:membershipId/deny-permissions', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + param('membershipId').isMongoId().exists().trim(), + body('permissions').isArray().exists(), + validateRequest, + EEMembershipControllers.denyMembershipPermissions +); + export default router; diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index 924db18fc..11afa4f2e 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -54,4 +54,17 @@ router.delete( environmentController.deleteWorkspaceEnvironment ); +router.get( + '/:workspaceId/environments', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [MEMBER, ADMIN], + }), + param('workspaceId').exists().trim(), + validateRequest, + environmentController.getAllAccessibleEnvironmentsOfWorkspace +); + export default router; diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 1b74bd3ee..806460997 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -6,6 +6,10 @@ const MEMBER = 'member'; // membership statuses const INVITED = 'invited'; +// membership permissions ability +const ABILITY_READ = 'read'; +const ABILITY_WRITE = 'write'; + // -- organization const ACCEPTED = 'accepted'; @@ -14,5 +18,7 @@ export { ADMIN, MEMBER, INVITED, - ACCEPTED + ACCEPTED, + ABILITY_READ, + ABILITY_WRITE } \ No newline at end of file