Skip to content

Commit

Permalink
Begin rewiring frontend to use batch route for CRUD secret ops
Browse files Browse the repository at this point in the history
  • Loading branch information
dangtony98 committed Feb 16, 2023
1 parent da857f3 commit 65bec23
Show file tree
Hide file tree
Showing 7 changed files with 413 additions and 28 deletions.
251 changes: 246 additions & 5 deletions backend/src/controllers/v2/secretsController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import to from 'await-to-js';
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import { ISecret, Membership, Secret, Workspace } from '../../models';
import { ISecret, Secret } from '../../models';
import { IAction } from '../../ee/models';
import {
SECRET_PERSONAL,
SECRET_SHARED,
Expand All @@ -20,6 +21,250 @@ import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _ from 'lodash';
import {
BatchSecretRequest,
BatchSecret
} from '../../types/secret';

/**
* Peform a batch of any specified CUD secret operations
* @param req
* @param res
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']);
const {
workspaceId,
environment,
requests
}: {
workspaceId: string;
environment: string;
requests: BatchSecretRequest[];
}= req.body;

// construct object containing all secrets
// listed across requests
const listedSecretsObj: {
[key: string]: {
version: number;
type: string;
}
} = (await Secret.find({
_id: {
$in: requests
.map((request) => request.secret._id)
.filter((secretId) => secretId !== undefined)
}
}).select('version type')).reduce((obj: any, secret: ISecret) => ({
...obj,
[secret._id.toString()]: secret
}), {});


const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
const deleteSecrets: Types.ObjectId[] = [];
const actions: IAction[] = [];

requests.forEach((request) => {
switch (request.method) {
case 'POST':
createSecrets.push({
...request.secret,
version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment,
workspace: new Types.ObjectId(workspaceId)
});
break;
case 'PATCH':
updateSecrets.push({
...request.secret,
_id: new Types.ObjectId(request.secret._id)
});
break;
case 'DELETE':
deleteSecrets.push(new Types.ObjectId(request.secret._id));
break;
}
});

// handle create secrets
let createdSecrets: ISecret[] = [];
if (createSecrets.length > 0) {
createdSecrets = await Secret.insertMany(createSecrets);
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: createdSecrets.map((n: any) => {
return ({
...n._doc,
_id: new Types.ObjectId(),
secret: n._id,
isDeleted: false
});
})
});

const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id)
}) as IAction;
actions.push(addAction);

if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: req.user.email,
properties: {
numberOfSecrets: createdSecrets.length,
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
}
});
}
}

// handle update secrets
let updatedSecrets: ISecret[] = [];
if (updateSecrets.length > 0) {
const updateOperations = updateSecrets.map((u) => ({
updateOne: {
filter: { _id: new Types.ObjectId(u._id) },
update: {
$inc: {
version: 1
},
...u,
_id: new Types.ObjectId(u._id)
}
}
}));

await Secret.bulkWrite(updateOperations);

const secretVersions = updateSecrets.map((u) => ({
secret: new Types.ObjectId(u._id),
version: listedSecretsObj[u._id.toString()].version,
workspace: new Types.ObjectId(workspaceId),
type: listedSecretsObj[u._id.toString()].type,
environment,
isDeleted: false,
secretKeyCiphertext: u.secretKeyCiphertext,
secretKeyIV: u.secretKeyIV,
secretKeyTag: u.secretKeyTag,
secretValueCiphertext: u.secretValueCiphertext,
secretValueIV: u.secretValueIV,
secretValueTag: u.secretValueTag,
secretCommentCiphertext: u.secretCommentCiphertext,
secretCommentIV: u.secretCommentIV,
secretCommentTag: u.secretCommentTag,
tags: u.tags
}));

await EESecretService.addSecretVersions({
secretVersions
});

updatedSecrets = await Secret.find({
_id: {
$in: updateSecrets.map((u) => new Types.ObjectId(u._id))
}
});

if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: req.user.email,
properties: {
numberOfSecrets: updateSecrets.length,
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
}
});
}
}

// handle delete secrets
if (deleteSecrets.length > 0) {
await Secret.deleteMany({
_id: {
$in: deleteSecrets
}
});

await EESecretService.markDeletedSecretVersions({
secretIds: deleteSecrets
});

const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: deleteSecrets
}) as IAction;
actions.push(deleteAction);

if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: req.user.email,
properties: {
numberOfSecrets: deleteSecrets.length,
environment,
workspaceId,
channel: channel,
userAgent: req.headers?.['user-agent']
}
});
}
}

if (actions.length > 1) {
// (EE) create (audit) log
await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId: new Types.ObjectId(workspaceId),
actions,
channel,
ipAddress: req.ip
});
}

// // trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId
})
});

// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});

const resObj: { [key: string]: ISecret[] | string[] } = {}

if (createSecrets.length > 0) {
resObj['createdSecrets'] = createdSecrets;
}

if (updateSecrets.length > 0) {
resObj['updatedSecrets'] = updatedSecrets;
}

if (deleteSecrets.length > 0) {
resObj['deletedSecrets'] = deleteSecrets.map((d) => d.toString());
}

return res.status(200).send(resObj);
}

/**
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
Expand Down Expand Up @@ -166,11 +411,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
Expand All @@ -187,11 +430,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
Expand Down
8 changes: 1 addition & 7 deletions backend/src/ee/controllers/v1/secretController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
} = oldSecretVersion;

// update secret
Expand All @@ -179,11 +177,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
},
{
new: true
Expand All @@ -204,11 +200,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
secretValueTag
}).save();

// take secret snapshot
Expand Down
11 changes: 1 addition & 10 deletions backend/src/ee/models/secretVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@ import {
} from '../../variables';

export interface ISecretVersion {
_id: Types.ObjectId;
secret: Types.ObjectId;
version: number;
workspace: Types.ObjectId; // new
type: string; // new
user: Types.ObjectId; // new
user?: Types.ObjectId; // new
environment: string; // new
isDeleted: boolean;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
tags?: string[];
}

Expand Down Expand Up @@ -72,9 +69,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
type: String, // symmetric
required: true
},
secretKeyHash: {
type: String
},
secretValueCiphertext: {
type: String,
required: true
Expand All @@ -87,9 +81,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
type: String, // symmetric
required: true
},
secretValueHash: {
type: String
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
Expand Down
18 changes: 18 additions & 0 deletions backend/src/routes/v2/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import {
SECRET_SHARED
} from '../../variables';

// TODO: create batch update endpoint

router.post(
'/batch',
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('requests').exists(), // perform validation for batch requests
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
secretsController.batchSecrets
)

router.post(
'/',
body('workspaceId').exists().isString().trim(),
Expand Down
Loading

0 comments on commit 65bec23

Please sign in to comment.