Update modify secrets api v2 so that fields are optional

This commit is contained in:
Maidul Islam
2023-01-12 15:31:58 -08:00
parent 47fd48b7b0
commit 71f60f1589
4 changed files with 141 additions and 158 deletions
+54 -59
View File
@@ -2,8 +2,8 @@ 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, Secret } from '../../models';
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
@@ -23,9 +23,9 @@ import { BadRequestError } from '../../utils/errors';
* @param res * @param res
*/ */
export const createSecrets = async (req: Request, res: Response) => { export const createSecrets = 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';
const { workspaceId, environment } = req.body; const { workspaceId, environment } = req.body;
let toAdd; let toAdd;
if (Array.isArray(req.body.secrets)) { if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets // case: create multiple secrets
@@ -34,7 +34,7 @@ export const createSecrets = async (req: Request, res: Response) => {
// case: create 1 secret // case: create 1 secret
toAdd = [req.body.secrets]; toAdd = [req.body.secrets];
} }
const newSecrets = await Secret.insertMany( const newSecrets = await Secret.insertMany(
toAdd.map(({ toAdd.map(({
type, type,
@@ -66,7 +66,7 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueTag secretValueTag
})) }))
); );
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
EESecretService.addSecretVersions({ EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({ secretVersions: newSecrets.map(({
@@ -160,7 +160,7 @@ export const createSecrets = async (req: Request, res: Response) => {
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.query; const { workspaceId, environment } = req.query;
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
if (req.user) { if (req.user) {
userId = req.user._id; userId = req.user._id;
@@ -169,13 +169,13 @@ export const getSecrets = async (req: Request, res: Response) => {
if (req.serviceTokenData) { if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id userId = req.serviceTokenData.user._id
} }
const [err, secrets] = await to(Secret.find( const [err, secrets] = await to(Secret.find(
{ {
workspace: workspaceId, workspace: workspaceId,
environment, environment,
$or: [ $or: [
{ user: userId }, { user: userId },
{ user: { $exists: false } } { user: { $exists: false } }
], ],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
@@ -183,9 +183,9 @@ export const getSecrets = async (req: Request, res: Response) => {
).then()) ).then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
const readAction = await EELogService.createActionSecret({ const readAction = await EELogService.createActionSecret({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
userId: req.user._id.toString(), userId: req.user._id.toString(),
@@ -214,7 +214,7 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets secrets
}); });
@@ -226,8 +226,8 @@ export const getSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const updateSecrets = async (req: Request, res: Response) => { 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;
@@ -242,7 +242,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentTag: string; secretCommentTag: string;
} }
const ops = req.body.secrets.map((secret: PatchSecret) => { const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
const { const {
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -254,6 +254,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentIV, secretCommentIV,
secretCommentTag secretCommentTag
} = secret; } = secret;
return ({ return ({
updateOne: { updateOne: {
filter: { _id: new Types.ObjectId(secret.id) }, filter: { _id: new Types.ObjectId(secret.id) },
@@ -268,8 +269,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
...(( ...((
secretCommentCiphertext && secretCommentCiphertext &&
secretCommentIV && secretCommentIV &&
secretCommentTag secretCommentTag
) ? { ) ? {
secretCommentCiphertext, secretCommentCiphertext,
@@ -280,15 +281,17 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
}); });
}); });
await Secret.bulkWrite(ops);
await Secret.bulkWrite(updateOperationsToPerform);
const newSecretsObj: { [key: string]: PatchSecret } = {};
const secretModificationsBySecretId: { [key: string]: PatchSecret } = {};
req.body.secrets.forEach((secret: PatchSecret) => { req.body.secrets.forEach((secret: PatchSecret) => {
newSecretsObj[secret.id] = secret; secretModificationsBySecretId[secret.id] = secret;
}); });
await EESecretService.addSecretVersions({ const ListOfSecretsBeforeModifications = req.secrets
secretVersions: req.secrets.map((secret: ISecret) => { const secretVersions = {
secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => {
const { const {
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -298,37 +301,29 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag secretCommentTag,
} = newSecretsObj[secret._id.toString()] } = secretModificationsBySecretId[secret._id.toString()]
return ({ return ({
secret: secret._id, secret: secret._id,
version: secret.version + 1, version: secret.version + 1,
workspace: secret.workspace, workspace: secret.workspace,
type: secret.type, type: secret.type,
environment: secret.environment, environment: secret.environment,
isDeleted: false, secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
secretKeyCiphertext, secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
secretKeyIV, secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
secretKeyTag, secretValueCiphertext: secretValueCiphertext ? secretValueCiphertext : secret.secretValueCiphertext,
secretValueCiphertext, secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV,
secretValueIV, secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag,
secretValueTag, secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
...(( secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentCiphertext && secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
secretCommentIV &&
secretCommentTag
) ? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} : {
secretCommentCiphertext: '',
secretCommentIV: '',
secretCommentTag: ''
})
}); });
}) })
}); }
await EESecretService.addSecretVersions(secretVersions);
// group secrets into workspaces so updated secrets can // group secrets into workspaces so updated secrets can
@@ -355,7 +350,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: key, workspaceId: key,
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
updateAction && await EELogService.createLog({ updateAction && await EELogService.createLog({
@@ -367,9 +362,9 @@ export const updateSecrets = async (req: Request, res: Response) => {
}); });
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: key workspaceId: key
}) })
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
@@ -385,7 +380,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
}); });
} }
}); });
return res.status(200).send({ return res.status(200).send({
secrets: await Secret.find({ secrets: await Secret.find({
_id: { _id: {
@@ -401,15 +396,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecrets = 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';
const toDelete = req.secrets.map((s: any) => s._id); const toDelete = req.secrets.map((s: any) => s._id);
await Secret.deleteMany({ await Secret.deleteMany({
_id: { _id: {
$in: toDelete $in: toDelete
} }
}); });
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: toDelete secretIds: toDelete
}); });
@@ -437,7 +432,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: key, workspaceId: key,
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
deleteAction && await EELogService.createLog({ deleteAction && await EELogService.createLog({
@@ -449,9 +444,9 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}); });
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: key workspaceId: key
}) })
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
@@ -467,7 +462,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}); });
} }
}); });
return res.status(200).send({ return res.status(200).send({
secrets: req.secrets secrets: req.secrets
}); });
+46 -46
View File
@@ -1,11 +1,11 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Secret, Secret,
ISecret ISecret
} from '../../models'; } from '../../models';
import { import {
SecretSnapshot, SecretSnapshot,
SecretVersion, SecretVersion,
ISecretVersion ISecretVersion
} from '../models'; } from '../models';
@@ -18,24 +18,24 @@ import {
* @param {String} obj.workspaceId * @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot * @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/ */
const takeSecretSnapshotHelper = async ({ const takeSecretSnapshotHelper = async ({
workspaceId workspaceId
}: { }: {
workspaceId: string; workspaceId: string;
}) => { }) => {
let secretSnapshot; let secretSnapshot;
try { try {
const secretIds = (await Secret.find({ const secretIds = (await Secret.find({
workspace: workspaceId workspace: workspaceId
}, '_id')).map((s) => s._id); }, '_id')).map((s) => s._id);
const latestSecretVersions = (await SecretVersion.aggregate([ const latestSecretVersions = (await SecretVersion.aggregate([
{ {
$match: { $match: {
secret: { secret: {
$in: secretIds $in: secretIds
} }
} }
}, },
{ {
@@ -48,14 +48,14 @@ import {
{ {
$sort: { version: -1 } $sort: { version: -1 }
} }
]) ])
.exec()) .exec())
.map((s) => s.versionId); .map((s) => s.versionId);
const latestSecretSnapshot = await SecretSnapshot.findOne({ const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId workspace: workspaceId
}).sort({ version: -1 }); }).sort({ version: -1 });
secretSnapshot = await new SecretSnapshot({ secretSnapshot = await new SecretSnapshot({
workspace: workspaceId, workspace: workspaceId,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
@@ -66,7 +66,7 @@ import {
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot'); throw new Error('Failed to take a secret snapshot');
} }
return secretSnapshot; return secretSnapshot;
} }
@@ -87,9 +87,9 @@ const addSecretVersionsHelper = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to add secret versions'); throw new Error(`Failed to add secret versions [err=${err}]`);
} }
return newSecretVersions; return newSecretVersions;
} }
@@ -120,39 +120,39 @@ const markDeletedSecretVersionsHelper = async ({
const initSecretVersioningHelper = async () => { const initSecretVersioningHelper = async () => {
try { try {
await Secret.updateMany( await Secret.updateMany(
{ version: { $exists: false } }, { version: { $exists: false } },
{ $set: { version: 1 } } { $set: { version: 1 } }
); );
const unversionedSecrets: ISecret[] = await Secret.aggregate([ const unversionedSecrets: ISecret[] = await Secret.aggregate([
{ {
$lookup: { $lookup: {
from: 'secretversions', from: 'secretversions',
localField: '_id', localField: '_id',
foreignField: 'secret', foreignField: 'secret',
as: 'versions', as: 'versions',
}, },
}, },
{ {
$match: { $match: {
versions: { $size: 0 }, versions: { $size: 0 },
}, },
}, },
]); ]);
if (unversionedSecrets.length > 0) { if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({ await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => ({ secretVersions: unversionedSecrets.map((s, idx) => ({
...s, ...s,
secret: s._id, secret: s._id,
version: s.version ? s.version : 1, version: s.version ? s.version : 1,
isDeleted: false, isDeleted: false,
workspace: s.workspace, workspace: s.workspace,
environment: s.environment environment: s.environment
})) }))
}); });
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -162,7 +162,7 @@ const initSecretVersioningHelper = async () => {
} }
export { export {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper, addSecretVersionsHelper,
markDeletedSecretVersionsHelper, markDeletedSecretVersionsHelper,
initSecretVersioningHelper initSecretVersioningHelper
+25 -25
View File
@@ -10,14 +10,14 @@ import {
export interface ISecretVersion { export interface ISecretVersion {
_id: Types.ObjectId; _id: Types.ObjectId;
secret: Types.ObjectId; secret: Types.ObjectId;
version: number; version: number;
workspace: Types.ObjectId; // new workspace: Types.ObjectId; // new
type: string; // new type: string; // new
user: Types.ObjectId; // new user: Types.ObjectId; // new
environment: string; // new environment: string; // new
isDeleted: boolean; isDeleted: boolean;
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
secretKeyHash: string; secretKeyHash: string;
@@ -28,17 +28,17 @@ export interface ISecretVersion {
} }
const secretVersionSchema = new Schema<ISecretVersion>( const secretVersionSchema = new Schema<ISecretVersion>(
{ {
secret: { // could be deleted secret: { // could be deleted
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Secret', ref: 'Secret',
required: true required: true
}, },
version: { version: {
type: Number, type: Number,
default: 1, default: 1,
required: true required: true
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: 'Workspace',
@@ -59,12 +59,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true required: true
}, },
isDeleted: { // consider removing field isDeleted: { // consider removing field
type: Boolean, type: Boolean,
default: false, default: false,
required: true required: true
}, },
secretKeyCiphertext: { secretKeyCiphertext: {
type: String, type: String,
required: true required: true
}, },
@@ -94,10 +94,10 @@ const secretVersionSchema = new Schema<ISecretVersion>(
secretValueHash: { secretValueHash: {
type: String type: String
} }
}, },
{ {
timestamps: true timestamps: true
} }
); );
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema); const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
+16 -28
View File
@@ -8,8 +8,8 @@ import {
} from '../../middleware'; } from '../../middleware';
import { query, check, body } from 'express-validator'; import { query, check, body } from 'express-validator';
import { secretsController } from '../../controllers/v2'; import { secretsController } from '../../controllers/v2';
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED SECRET_SHARED
@@ -27,7 +27,7 @@ router.post(
if (value.length === 0) throw new Error('secrets cannot be an empty array') if (value.length === 0) throw new Error('secrets cannot be an empty array')
for (const secret of value) { for (const secret of value) {
if ( if (
!secret.type || !secret.type ||
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) || !(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
!secret.secretKeyCiphertext || !secret.secretKeyCiphertext ||
!secret.secretKeyIV || !secret.secretKeyIV ||
@@ -42,7 +42,7 @@ router.post(
} else if (typeof value === 'object') { } else if (typeof value === 'object') {
// case: update 1 secret // case: update 1 secret
if ( if (
!value.type || !value.type ||
!(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) || !(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) ||
!value.secretKeyCiphertext || !value.secretKeyCiphertext ||
!value.secretKeyIV || !value.secretKeyIV ||
@@ -52,13 +52,13 @@ router.post(
!value.secretValueTag !value.secretValueTag
) { ) {
throw new Error('secrets object is missing required secret properties'); throw new Error('secrets object is missing required secret properties');
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects') throw new Error('secrets must be an object or an array of objects')
} }
return true; return true;
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
@@ -95,36 +95,24 @@ router.patch(
if (value.length === 0) throw new Error('secrets cannot be an empty array') if (value.length === 0) throw new Error('secrets cannot be an empty array')
for (const secret of value) { for (const secret of value) {
if ( if (
!secret.id || !secret.id
!secret.secretKeyCiphertext ||
!secret.secretKeyIV ||
!secret.secretKeyTag ||
!secret.secretValueCiphertext ||
!secret.secretValueIV ||
!secret.secretValueTag
) { ) {
throw new Error('secrets array must contain objects that have required secret properties'); throw new Error('Each secret must contain a ID property');
} }
} }
} else if (typeof value === 'object') { } else if (typeof value === 'object') {
// case: update 1 secret // case: update 1 secret
if ( if (
!value.id || !value.id
!value.secretKeyCiphertext ||
!value.secretKeyIV ||
!value.secretKeyTag ||
!value.secretValueCiphertext ||
!value.secretValueIV ||
!value.secretValueTag
) { ) {
throw new Error('secrets object is missing required secret properties'); throw new Error('secret must contain a ID property');
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects') throw new Error('secrets must be an object or an array of objects')
} }
return true; return true;
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
@@ -142,13 +130,13 @@ router.delete(
.custom((value) => { .custom((value) => {
// case: delete 1 secret // case: delete 1 secret
if (typeof value === 'string') return true; if (typeof value === 'string') return true;
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: delete multiple secrets // case: delete multiple secrets
if (value.length === 0) throw new Error('secrets cannot be an empty array'); if (value.length === 0) throw new Error('secrets cannot be an empty array');
return value.every((id: string) => typeof id === 'string') return value.every((id: string) => typeof id === 'string')
} }
throw new Error('secretIds must be a string or an array of strings'); throw new Error('secretIds must be a string or an array of strings');
}) })
.not() .not()