Skip to content

Commit

Permalink
Add tags support for secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
maidul98 committed Feb 5, 2023
1 parent 8ae43cd commit 9f9273b
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 26 deletions.
2 changes: 2 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';

import { healthCheck } from './routes/status';
Expand Down Expand Up @@ -112,6 +113,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
Expand Down
4 changes: 3 additions & 1 deletion backend/src/controllers/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
import * as tagController from './tagController';

export {
usersController,
Expand All @@ -15,5 +16,6 @@ export {
apiKeyDataController,
secretController,
secretsController,
environmentController
environmentController,
tagController
}
58 changes: 35 additions & 23 deletions backend/src/controllers/v2/secretsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,33 +86,37 @@ export const createSecrets = async (req: Request, res: Response) => {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}

let toAdd;
let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets
toAdd = req.body.secrets;
listOfSecretsToCreate = req.body.secrets;
} else if (typeof req.body.secrets === 'object') {
// case: create 1 secret
toAdd = [req.body.secrets];
listOfSecretsToCreate = [req.body.secrets];
}

const newSecrets = await Secret.insertMany(
toAdd.map(({
type secretsToCreateType = {
type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
tags: string[]
}

const newlyCreatedSecrets = await Secret.insertMany(
listOfSecretsToCreate.map(({
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
}: {
type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}) => {
tags
}: secretsToCreateType) => {
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
Expand All @@ -124,7 +128,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
tags
});
})
);
Expand All @@ -140,7 +145,7 @@ export const createSecrets = async (req: Request, res: Response) => {

// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({
secretVersions: newlyCreatedSecrets.map(({
_id,
version,
workspace,
Expand All @@ -154,7 +159,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
secretValueHash,
tags
}) => ({
_id: new Types.ObjectId(),
secret: _id,
Expand All @@ -171,15 +177,16 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
secretValueHash,
tags
}))
});

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

// (EE) create (audit) log
Expand All @@ -201,7 +208,7 @@ export const createSecrets = async (req: Request, res: Response) => {
event: 'secrets added',
distinctId: req.user.email,
properties: {
numberOfSecrets: toAdd.length,
numberOfSecrets: listOfSecretsToCreate.length,
environment,
workspaceId,
channel: channel,
Expand All @@ -211,7 +218,7 @@ export const createSecrets = async (req: Request, res: Response) => {
}

return res.status(200).send({
secrets: newSecrets
secrets: newlyCreatedSecrets
});
}

Expand Down Expand Up @@ -294,7 +301,7 @@ export const getSecrets = async (req: Request, res: Response) => {
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).then())
).populate("tags").then())

if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });

Expand Down Expand Up @@ -398,6 +405,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[]
}

const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
Expand All @@ -410,7 +418,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
secretCommentTag,
tags
} = secret;

return ({
Expand All @@ -426,6 +435,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
tags,
...((
secretCommentCiphertext &&
secretCommentIV &&
Expand Down Expand Up @@ -460,6 +470,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
} = secretModificationsBySecretId[secret._id.toString()]

return ({
Expand All @@ -477,6 +488,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
tags: tags ? tags : secret.tags
});
})
}
Expand Down
66 changes: 66 additions & 0 deletions backend/src/controllers/v2/tagController.ts
Original file line number Diff line number Diff line change
@@ -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
})
}
8 changes: 7 additions & 1 deletion backend/src/ee/models/secretVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ISecretVersion {
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
tags?: string[];
}

const secretVersionSchema = new Schema<ISecretVersion>(
Expand Down Expand Up @@ -88,7 +89,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
},
secretValueHash: {
type: String
}
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
},
{
timestamps: true
Expand Down
6 changes: 6 additions & 0 deletions backend/src/models/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface ISecret {
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
tags?: string[];
}

const secretSchema = new Schema<ISecret>(
Expand All @@ -47,6 +48,11 @@ const secretSchema = new Schema<ISecret>(
type: Schema.Types.ObjectId,
ref: 'User'
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
environment: {
type: String,
required: true
Expand Down
49 changes: 49 additions & 0 deletions backend/src/models/tag.ts
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 3 additions & 1 deletion backend/src/routes/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import secrets from './secrets';
import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
import environment from "./environment"
import tags from "./tags"

export {
users,
Expand All @@ -15,5 +16,6 @@ export {
secrets,
serviceTokenData,
apiKeyData,
environment
environment,
tags
}
Loading

0 comments on commit 9f9273b

Please sign in to comment.