Add deny api/get envs api

This commit is contained in:
Maidul Islam
2023-01-29 21:12:41 -08:00
parent 5162ba9b91
commit a940fa210a
14 changed files with 303 additions and 76 deletions
+25
View File
@@ -19,6 +19,7 @@
"axios": "^1.1.3", "axios": "^1.1.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
@@ -32,6 +33,7 @@
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2", "mongoose": "^6.7.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.2.2", "posthog-node": "^2.2.2",
@@ -58,6 +60,7 @@
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/jest": "^29.2.4", "@types/jest": "^29.2.4",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
@@ -3221,6 +3224,12 @@
"resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
"integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" "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": { "node_modules/@types/long": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@@ -4085,6 +4094,11 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
"integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" "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": { "@types/long": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@@ -15269,6 +15289,11 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "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": { "bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+3
View File
@@ -36,6 +36,7 @@
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/jest": "^29.2.4", "@types/jest": "^29.2.4",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
@@ -85,6 +86,7 @@
"axios": "^1.1.3", "axios": "^1.1.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
@@ -98,6 +100,7 @@
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2", "mongoose": "^6.7.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.2.2", "posthog-node": "^2.2.2",
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key } from '../../models'; import { Membership, MembershipOrg, User, Key, IMembership, Workspace } from '../../models';
import { import {
findMembership, findMembership,
deleteMembership as deleteMember deleteMembership as deleteMember
@@ -230,4 +230,4 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
invitee, invitee,
latestKey latestKey
}); });
}; };
@@ -6,8 +6,12 @@ import {
Workspace, Workspace,
Integration, Integration,
ServiceTokenData, ServiceTokenData,
Membership,
} from '../../models'; } from '../../models';
import { SecretVersion } from '../../ee/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 * Create new workspace environment named [environmentName] under workspace with id
@@ -120,6 +124,15 @@ export const renameWorkspaceEnvironment = async (
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { environment: environmentSlug }
); );
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -188,6 +201,11 @@ export const deleteWorkspaceEnvironment = async (
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -202,3 +220,38 @@ export const deleteWorkspaceEnvironment = async (
environment: environmentSlug, 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 })
};
@@ -1,7 +1,7 @@
import to from 'await-to-js'; import to from 'await-to-js';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ISecret, Secret } from '../../models'; import { ISecret, Membership, Secret, Workspace } from '../../models';
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
@@ -10,13 +10,14 @@ import {
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS ACTION_DELETE_SECRETS
} from '../../variables'; } from '../../variables';
import { ValidationError } from '../../utils/errors'; import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services'; import { EESecretService, EELogService } from '../../ee/services';
import { postHogClient } from '../../services'; import { postHogClient } from '../../services';
import { BadRequestError } from '../../utils/errors';
import { getChannelFromUserAgent } from '../../utils/posthog'; 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] * 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 channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment } = req.body; 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; let toAdd;
if (Array.isArray(req.body.secrets)) { if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets // case: create multiple secrets
@@ -269,6 +276,14 @@ export const getSecrets = async (req: Request, res: Response) => {
userEmail = req.serviceTokenData.user.email; 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( const [err, secrets] = await to(Secret.find(
{ {
workspace: workspaceId, 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'; const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type // TODO: move type
interface PatchSecret { interface PatchSecret {
id: string; id: string;
+3 -1
View File
@@ -3,11 +3,13 @@ import * as secretController from './secretController';
import * as secretSnapshotController from './secretSnapshotController'; import * as secretSnapshotController from './secretSnapshotController';
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as actionController from './actionController'; import * as actionController from './actionController';
import * as membershipController from './membershipController';
export { export {
stripeController, stripeController,
secretController, secretController,
secretSnapshotController, secretSnapshotController,
workspaceController, workspaceController,
actionController actionController,
membershipController
} }
@@ -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<IMembershipPermission>()
.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
})
}
@@ -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
}
+61 -46
View File
@@ -12,14 +12,17 @@ import {
import { import {
IAction IAction
} from '../ee/models'; } from '../ee/models';
import { import {
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS ACTION_READ_SECRETS
} from '../variables'; } 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] * Validate that user with id [userId] can modify secrets with ids [secretIds]
@@ -34,7 +37,7 @@ const validateSecrets = async ({
}: { }: {
userId: string; userId: string;
secretIds: string[]; secretIds: string[];
}) =>{ }) => {
let secrets; let secrets;
try { try {
secrets = await Secret.find({ secrets = await Secret.find({
@@ -42,20 +45,31 @@ const validateSecrets = async ({
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
} }
}); });
const workspaceIdsSet = new Set((await Membership.find({ if (secrets.length != secretIds.length) {
user: userId throw BadRequestError({ message: 'Unable to validate some secrets' })
}, 'workspace')) }
.map((m) => m.workspace.toString()));
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) => { secrets.forEach((secret: ISecret) => {
if (!workspaceIdsSet.has(secret.workspace.toString())) { if (workspaceIdsSet.has(secret.workspace.toString())) {
throw new Error('Failed to validate secret'); 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) { } catch (err) {
throw new Error('Failed to validate secrets'); throw BadRequestError({ message: 'Unable to validate secrets' })
} }
return secrets; return secrets;
@@ -127,13 +141,13 @@ const v1PushSecrets = async ({
workspaceId, workspaceId,
environment environment
}); });
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) ({ ...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 }) ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s })
, {}); , {});
// handle deleting secrets // handle deleting secrets
const toDelete = oldSecrets const toDelete = oldSecrets
@@ -150,12 +164,12 @@ const v1PushSecrets = async ({
secretIds: toDelete secretIds: toDelete
}); });
} }
const toUpdate = oldSecrets const toUpdate = oldSecrets
.filter((s) => { .filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) { || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
// case: filter secrets where value or comment changed // case: filter secrets where value or comment changed
return true; return true;
} }
@@ -165,7 +179,7 @@ const v1PushSecrets = async ({
return true; return true;
} }
} }
return false; return false;
}); });
@@ -217,7 +231,7 @@ const v1PushSecrets = async ({
}; };
}); });
await Secret.bulkWrite(operations as any); await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets // (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: toUpdate.map(({ secretVersions: toUpdate.map(({
@@ -245,7 +259,7 @@ const v1PushSecrets = async ({
secretValueTag: newSecret.tagValue, secretValueTag: newSecret.tagValue,
secretValueHash: newSecret.hashValue secretValueHash: newSecret.hashValue
}) })
}) })
}); });
// handle adding new secrets // handle adding new secrets
@@ -319,7 +333,7 @@ const v1PushSecrets = async ({
})) }))
}); });
} }
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId workspaceId
@@ -344,7 +358,7 @@ const v1PushSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto) * @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets * @param {String} obj.ipAddress - ip address of request to push secrets
*/ */
const v2PushSecrets = async ({ const v2PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -362,20 +376,20 @@ const v1PushSecrets = async ({
// TODO: clean up function and fix up types // TODO: clean up function and fix up types
try { try {
const actions: IAction[] = []; const actions: IAction[] = [];
// construct useful data structures // construct useful data structures
const oldSecrets = await getSecrets({ const oldSecrets = await getSecrets({
userId, userId,
workspaceId, workspaceId,
environment environment
}); });
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) ({ ...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 }) ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {}); , {});
// handle deleting secrets // handle deleting secrets
const toDelete = oldSecrets const toDelete = oldSecrets
@@ -391,7 +405,7 @@ const v1PushSecrets = async ({
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: toDelete secretIds: toDelete
}); });
const deleteAction = await EELogService.createActionSecret({ const deleteAction = await EELogService.createActionSecret({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId, userId,
@@ -401,12 +415,12 @@ const v1PushSecrets = async ({
deleteAction && actions.push(deleteAction); deleteAction && actions.push(deleteAction);
} }
const toUpdate = oldSecrets const toUpdate = oldSecrets
.filter((s) => { .filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) { || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
// case: filter secrets where value or comment changed // case: filter secrets where value or comment changed
return true; return true;
} }
@@ -416,7 +430,7 @@ const v1PushSecrets = async ({
return true; return true;
} }
} }
return false; return false;
}); });
@@ -469,7 +483,7 @@ const v1PushSecrets = async ({
}; };
}); });
await Secret.bulkWrite(operations as any); await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets // (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => { secretVersions: toUpdate.map((s) => {
@@ -482,7 +496,7 @@ const v1PushSecrets = async ({
environment: s.environment, environment: s.environment,
isDeleted: false isDeleted: false
}) })
}) })
}); });
const updateAction = await EELogService.createActionSecret({ const updateAction = await EELogService.createActionSecret({
@@ -507,18 +521,19 @@ const v1PushSecrets = async ({
workspace: workspaceId, workspace: workspaceId,
type: toAdd[idx].type, type: toAdd[idx].type,
environment, environment,
...( toAdd[idx].type === 'personal' ? { user: userId } : {}) ...(toAdd[idx].type === 'personal' ? { user: userId } : {})
})) }))
); );
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
EESecretService.addSecretVersions({ EESecretService.addSecretVersions({
secretVersions: newSecrets.map((secretDocument) => { secretVersions: newSecrets.map((secretDocument) => {
return { return {
...secretDocument.toObject(), ...secretDocument.toObject(),
secret: secretDocument._id, secret: secretDocument._id,
isDeleted: false isDeleted: false
}}) }
})
}); });
const addAction = await EELogService.createActionSecret({ const addAction = await EELogService.createActionSecret({
@@ -529,7 +544,7 @@ const v1PushSecrets = async ({
}); });
addAction && actions.push(addAction); addAction && actions.push(addAction);
} }
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId workspaceId
@@ -560,7 +575,7 @@ const v1PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
*/ */
const getSecrets = async ({ const getSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment environment
@@ -570,7 +585,7 @@ const v1PushSecrets = async ({
environment: string; environment: string;
}): Promise<ISecret[]> => { }): Promise<ISecret[]> => {
let secrets: any; // TODO: FIX any let secrets: any; // TODO: FIX any
try { try {
// get shared workspace secrets // get shared workspace secrets
const sharedSecrets = await Secret.find({ const sharedSecrets = await Secret.find({
@@ -622,7 +637,7 @@ const pullSecrets = async ({
ipAddress: string; ipAddress: string;
}): Promise<ISecret[]> => { }): Promise<ISecret[]> => {
let secrets: any; let secrets: any;
try { try {
secrets = await getSecrets({ secrets = await getSecrets({
userId, userId,
+19 -1
View File
@@ -1,15 +1,21 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ADMIN, MEMBER } from '../variables'; import { ADMIN, MEMBER } from '../variables';
export interface IMembershipPermission {
environmentSlug: string,
ability: string
}
export interface IMembership { export interface IMembership {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail?: string; inviteEmail?: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: 'admin' | 'member'; role: 'admin' | 'member';
deniedPermissions: IMembershipPermission[]
} }
const membershipSchema = new Schema( const membershipSchema = new Schema<IMembership>(
{ {
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
@@ -23,6 +29,18 @@ const membershipSchema = new Schema(
ref: 'Workspace', ref: 'Workspace',
required: true required: true
}, },
deniedPermissions: {
type: [
{
environmentSlug: String,
ability: {
type: String,
enum: ['read', 'write']
},
},
],
default: []
},
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER], enum: [ADMIN, MEMBER],
-15
View File
@@ -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";
}
+18 -6
View File
@@ -3,14 +3,15 @@ const router = express.Router();
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../../controllers/v1'; import { membershipController } from '../../controllers/v1';
import { membershipController as EEMembershipControllers } from '../../ee/controllers/v1';
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
router.get( // used for old CLI (deprecate) router.get( // used for old CLI (deprecate)
'/:workspaceId/connect', '/:workspaceId/connect',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
membershipController.validateMembership membershipController.validateMembership
@@ -19,8 +20,8 @@ router.get( // used for old CLI (deprecate)
router.delete( router.delete(
'/:membershipId', '/:membershipId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
validateRequest, validateRequest,
membershipController.deleteMembership membershipController.deleteMembership
@@ -29,11 +30,22 @@ router.delete(
router.post( router.post(
'/:membershipId/change-role', '/:membershipId/change-role',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
body('role').exists().trim(), body('role').exists().trim(),
validateRequest, validateRequest,
membershipController.changeMembershipRole 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; export default router;
+13
View File
@@ -54,4 +54,17 @@ router.delete(
environmentController.deleteWorkspaceEnvironment environmentController.deleteWorkspaceEnvironment
); );
router.get(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
}),
param('workspaceId').exists().trim(),
validateRequest,
environmentController.getAllAccessibleEnvironmentsOfWorkspace
);
export default router; export default router;
+7 -1
View File
@@ -6,6 +6,10 @@ const MEMBER = 'member';
// membership statuses // membership statuses
const INVITED = 'invited'; const INVITED = 'invited';
// membership permissions ability
const ABILITY_READ = 'read';
const ABILITY_WRITE = 'write';
// -- organization // -- organization
const ACCEPTED = 'accepted'; const ACCEPTED = 'accepted';
@@ -14,5 +18,7 @@ export {
ADMIN, ADMIN,
MEMBER, MEMBER,
INVITED, INVITED,
ACCEPTED ACCEPTED,
ABILITY_READ,
ABILITY_WRITE
} }