mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
Add tags support for secrets
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user