Add tags support for secrets

This commit is contained in:
Maidul Islam
2023-02-05 12:54:42 -08:00
parent 8ae43cdcf6
commit 9f9273bb02
9 changed files with 221 additions and 26 deletions
+2
View File
@@ -50,6 +50,7 @@ import {
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter, apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter, environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2'; } from './routes/v2';
import { healthCheck } from './routes/status'; import { healthCheck } from './routes/status';
@@ -112,6 +113,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v2/users', v2UsersRouter); app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter); app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter); app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter); app.use('/api/v2/secrets', v2SecretsRouter);
+3 -1
View File
@@ -6,6 +6,7 @@ import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretsController from './secretsController'; import * as secretsController from './secretsController';
import * as environmentController from './environmentController'; import * as environmentController from './environmentController';
import * as tagController from './tagController';
export { export {
usersController, usersController,
@@ -15,5 +16,6 @@ export {
apiKeyDataController, apiKeyDataController,
secretController, secretController,
secretsController, secretsController,
environmentController environmentController,
tagController
} }
+35 -23
View File
@@ -86,17 +86,28 @@ export const createSecrets = async (req: Request, res: Response) => {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
} }
let toAdd; let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) { if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets // case: create multiple secrets
toAdd = req.body.secrets; listOfSecretsToCreate = req.body.secrets;
} else if (typeof req.body.secrets === 'object') { } else if (typeof req.body.secrets === 'object') {
// case: create 1 secret // case: create 1 secret
toAdd = [req.body.secrets]; listOfSecretsToCreate = [req.body.secrets];
} }
const newSecrets = await Secret.insertMany( type secretsToCreateType = {
toAdd.map(({ type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
tags: string[]
}
const newlyCreatedSecrets = await Secret.insertMany(
listOfSecretsToCreate.map(({
type, type,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -104,15 +115,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
}: { tags
type: string; }: secretsToCreateType) => {
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}) => {
return ({ return ({
version: 1, version: 1,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
@@ -124,7 +128,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
tags
}); });
}) })
); );
@@ -140,7 +145,7 @@ export const createSecrets = async (req: Request, res: Response) => {
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({ secretVersions: newlyCreatedSecrets.map(({
_id, _id,
version, version,
workspace, workspace,
@@ -154,7 +159,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash secretValueHash,
tags
}) => ({ }) => ({
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
secret: _id, secret: _id,
@@ -171,7 +177,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash secretValueHash,
tags
})) }))
}); });
@@ -179,7 +186,7 @@ export const createSecrets = async (req: Request, res: Response) => {
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
userId: req.user._id, userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: newSecrets.map((n) => n._id) secretIds: newlyCreatedSecrets.map((n) => n._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -201,7 +208,7 @@ export const createSecrets = async (req: Request, res: Response) => {
event: 'secrets added', event: 'secrets added',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: toAdd.length, numberOfSecrets: listOfSecretsToCreate.length,
environment, environment,
workspaceId, workspaceId,
channel: channel, channel: channel,
@@ -211,7 +218,7 @@ export const createSecrets = async (req: Request, res: Response) => {
} }
return res.status(200).send({ return res.status(200).send({
secrets: newSecrets secrets: newlyCreatedSecrets
}); });
} }
@@ -294,7 +301,7 @@ export const getSecrets = async (req: Request, res: Response) => {
], ],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
} }
).then()) ).populate("tags").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 });
@@ -398,6 +405,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: string; secretCommentCiphertext: string;
secretCommentIV: string; secretCommentIV: string;
secretCommentTag: string; secretCommentTag: string;
tags: string[]
} }
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => { const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
@@ -410,7 +418,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag secretCommentTag,
tags
} = secret; } = secret;
return ({ return ({
@@ -426,6 +435,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
tags,
...(( ...((
secretCommentCiphertext && secretCommentCiphertext &&
secretCommentIV && secretCommentIV &&
@@ -460,6 +470,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
tags
} = secretModificationsBySecretId[secret._id.toString()] } = secretModificationsBySecretId[secret._id.toString()]
return ({ return ({
@@ -477,6 +488,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
tags: tags ? tags : secret.tags
}); });
}) })
} }
@@ -0,0 +1,66 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership,
} from '../../models';
import Tag, { ITag } from '../../models/tag';
import { Builder } from "builder-pattern"
import to from 'await-to-js';
import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors';
import { MongoError } from 'mongodb';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
export const createWorkspaceTag = async (req: Request, res: Response) => {
const { workspaceId } = req.params
const { name, slug } = req.body
const sanitizedTagToCreate = Builder<ITag>()
.name(name)
.workspace(new Types.ObjectId(workspaceId))
.slug(slug)
.user(new Types.ObjectId(req.user._id))
.build();
const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate))
if (err) {
if ((err as MongoError).code === 11000) {
throw BadRequestError({ message: "Tags must be unique in a workspace" })
}
throw err
}
res.json(createdTag)
}
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const { tagId } = req.params
const tagFromDB = await Tag.findById(tagId)
if (!tagFromDB) {
throw BadRequestError()
}
// can only delete if the request user is one that belongs to the same workspace as the tag
const membership = await Membership.findOne({
user: req.user,
workspace: tagFromDB.workspace
});
if (!membership) {
UnauthorizedRequestError({ message: 'Failed to validate membership' });
}
await Tag.findByIdAndDelete(tagId)
res.sendStatus(200)
}
export const getWorkspaceTags = async (req: Request, res: Response) => {
const { workspaceId } = req.params
const workspaceTags = await Tag.find({ workspace: workspaceId })
return res.json({
workspaceTags
})
}
+7 -1
View File
@@ -21,6 +21,7 @@ export interface ISecretVersion {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
tags?: string[];
} }
const secretVersionSchema = new Schema<ISecretVersion>( const secretVersionSchema = new Schema<ISecretVersion>(
@@ -88,7 +89,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
}, },
secretValueHash: { secretValueHash: {
type: String type: String
} },
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
}, },
{ {
timestamps: true timestamps: true
+6
View File
@@ -23,6 +23,7 @@ export interface ISecret {
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
tags?: string[];
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
@@ -47,6 +48,11 @@ const secretSchema = new Schema<ISecret>(
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User' ref: 'User'
}, },
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
environment: { environment: {
type: String, type: String,
required: true required: true
+49
View File
@@ -0,0 +1,49 @@
import { Schema, model, Types } from 'mongoose';
export interface ITag {
_id: Types.ObjectId;
name: string;
slug: string;
user: Types.ObjectId;
workspace: Types.ObjectId;
}
const tagSchema = new Schema<ITag>(
{
name: {
type: String,
required: true,
trim: true,
},
slug: {
type: String,
required: true,
trim: true,
lowercase: true,
validate: [
function (value: any) {
return value.indexOf(' ') === -1;
},
'slug cannot contain spaces'
]
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
},
},
{
timestamps: true
}
);
tagSchema.index({ slug: 1, workspace: 1 }, { unique: true })
tagSchema.index({ workspace: 1 })
const Tag = model<ITag>('Tag', tagSchema);
export default Tag;
+3 -1
View File
@@ -6,6 +6,7 @@ import secrets from './secrets';
import serviceTokenData from './serviceTokenData'; import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData'; import apiKeyData from './apiKeyData';
import environment from "./environment" import environment from "./environment"
import tags from "./tags"
export { export {
users, users,
@@ -15,5 +16,6 @@ export {
secrets, secrets,
serviceTokenData, serviceTokenData,
apiKeyData, apiKeyData,
environment environment,
tags
} }
+50
View File
@@ -0,0 +1,50 @@
import express, { Response, Request } from 'express';
const router = express.Router();
import { body, param } from 'express-validator';
import { tagController } from '../../controllers/v2';
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
router.get(
'/:workspaceId/tags',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
}),
param('workspaceId').exists().trim(),
validateRequest,
tagController.getWorkspaceTags
);
router.delete(
'/tags/:tagId',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
param('tagId').exists().trim(),
validateRequest,
tagController.deleteWorkspaceTag
);
router.post(
'/:workspaceId/tags',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
}),
param('workspaceId').exists().trim(),
body('name').exists().trim(),
body('slug').exists().trim(),
validateRequest,
tagController.createWorkspaceTag
);
export default router;