mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add workspaces v3 endpoints for blind-index naming/labeling
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import * as secretsController from './secretsController';
|
||||
import * as workspacesController from './workspacesController';
|
||||
|
||||
export {
|
||||
secretsController
|
||||
secretsController,
|
||||
workspacesController
|
||||
}
|
||||
94
backend/src/controllers/v3/workspacesController.ts
Normal file
94
backend/src/controllers/v3/workspacesController.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import { Secret, Workspace, SecretBlindIndexData } from '../../models';
|
||||
import { SecretService } from'../../services';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import { decryptSymmetric } from '../../utils/crypto';
|
||||
import { getEncryptionKey } from '../../config';
|
||||
import * as argon2 from 'argon2';
|
||||
|
||||
/**
|
||||
* Return whether or not all secrets in workspace with id [workspaceId]
|
||||
* are blind-indexed
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const secretsWithoutBlindIndex = await Secret.countDocuments({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const isBlindIndexed = secretsWithoutBlindIndex === 0;
|
||||
|
||||
return res.status(200).send(isBlindIndexed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all secrets for workspace with id [workspaceId]
|
||||
*/
|
||||
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const secrets = await Secret.find({
|
||||
workspace: new Types.ObjectId (workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update blind indices for secrets in workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
interface SecretToUpdate {
|
||||
secretName: string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
const { workspaceId } = req.params;
|
||||
const {
|
||||
secretsToUpdate
|
||||
}: {
|
||||
secretsToUpdate: SecretToUpdate[];
|
||||
} = req.body;
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
// update secret blind indices
|
||||
const operations = await Promise.all(
|
||||
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => {
|
||||
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||
secretName: secretToUpdate.secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
return ({
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: new Types.ObjectId(secretToUpdate._id)
|
||||
},
|
||||
update: {
|
||||
secretBlindIndex
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Secret.bulkWrite(operations);
|
||||
|
||||
return res.status(200).send({
|
||||
operations
|
||||
});
|
||||
}
|
||||
@@ -218,7 +218,7 @@ const validateClientForSecrets = async ({
|
||||
}
|
||||
|
||||
/**
|
||||
* Create secret blind index data containing encrypted blind index salt
|
||||
* Create secret blind index data containing encrypted blind index [salt]
|
||||
* for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
@@ -250,6 +250,64 @@ const createSecretBlindIndexDataHelper = async ({
|
||||
return secretBlindIndexData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secret blind index salt for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
||||
* @returns
|
||||
*/
|
||||
const getSecretBlindIndexSaltHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) => {
|
||||
// check if workspace blind index data exists
|
||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||
|
||||
// decrypt workspace salt
|
||||
const salt = decryptSymmetric({
|
||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||
iv: secretBlindIndexData.saltIV,
|
||||
tag: secretBlindIndexData.saltTag,
|
||||
key: getEncryptionKey()
|
||||
});
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate blind index for secret with name [secretName]
|
||||
* and salt [salt]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretName - name of secret to generate blind index for
|
||||
* @param {String} obj.salt - base64-salt
|
||||
*/
|
||||
const generateSecretBlindIndexWithSaltHelper = async ({
|
||||
secretName,
|
||||
salt
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
}) => {
|
||||
|
||||
// generate secret blind index
|
||||
const secretBlindIndex = (await argon2.hash(secretName, {
|
||||
type: argon2.argon2id,
|
||||
salt: Buffer.from(salt, 'base64'),
|
||||
saltLength: 16, // default 16 bytes
|
||||
memoryCost: 65536, // default pool of 64 MiB per thread.
|
||||
hashLength: 32,
|
||||
parallelism: 1,
|
||||
raw: true
|
||||
})).toString('base64');
|
||||
|
||||
return secretBlindIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate blind index for secret with name [secretName]
|
||||
* for workspace with id [workspaceId]
|
||||
@@ -280,16 +338,10 @@ const generateSecretBlindIndexHelper = async ({
|
||||
key: getEncryptionKey()
|
||||
});
|
||||
|
||||
// generate secret blind index
|
||||
const secretBlindIndex = (await argon2.hash(secretName, {
|
||||
type: argon2.argon2id,
|
||||
salt: Buffer.from(salt, 'base64'),
|
||||
saltLength: 16, // default 16 bytes
|
||||
memoryCost: 65536, // default pool of 64 MiB per thread.
|
||||
hashLength: 32,
|
||||
parallelism: 1,
|
||||
raw: true
|
||||
})).toString('base64');
|
||||
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
return secretBlindIndex;
|
||||
}
|
||||
@@ -858,6 +910,8 @@ export {
|
||||
validateClientForSecret,
|
||||
validateClientForSecrets,
|
||||
createSecretBlindIndexDataHelper,
|
||||
getSecretBlindIndexSaltHelper,
|
||||
generateSecretBlindIndexWithSaltHelper,
|
||||
generateSecretBlindIndexHelper,
|
||||
createSecretHelper,
|
||||
getSecretsHelper,
|
||||
|
||||
@@ -67,12 +67,16 @@ const validateClientForWorkspace = async ({
|
||||
message: 'Failed to find workspace'
|
||||
});
|
||||
|
||||
if (requireBlindIndicesEnabled && !workspace.isBlindedIndicesEnabled) {
|
||||
if (requireBlindIndicesEnabled) {
|
||||
// case: blind indices are not enabled for secrets in this workspace
|
||||
// (i.e. workspace was created before blind indices were introduced
|
||||
// and no admin has enabled it)
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
const secretBlindIndexData = await SecretBlindIndexData.exists({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!secretBlindIndexData) throw UnauthorizedRequestError({
|
||||
message: 'Failed workspace authorization due to blind indices not being enabled'
|
||||
});
|
||||
}
|
||||
@@ -148,8 +152,7 @@ const createWorkspace = async ({
|
||||
workspace = await new Workspace({
|
||||
name,
|
||||
organization: organizationId,
|
||||
autoCapitalization: true,
|
||||
isBlindedIndicesEnabled: true
|
||||
autoCapitalization: true
|
||||
}).save();
|
||||
|
||||
// initialize bot for workspace
|
||||
|
||||
@@ -62,7 +62,8 @@ import {
|
||||
tags as v2TagsRouter,
|
||||
} from './routes/v2';
|
||||
import {
|
||||
secrets as v3SecretsRouter
|
||||
secrets as v3SecretsRouter,
|
||||
workspaces as v3WorkspacesRouter
|
||||
} from './routes/v3';
|
||||
import { healthCheck } from './routes/status';
|
||||
import { getLogger } from './utils/logger';
|
||||
@@ -159,6 +160,7 @@ const main = async () => {
|
||||
|
||||
// v3 routes (experimental)
|
||||
app.use('/api/v3/secrets', v3SecretsRouter);
|
||||
app.use('/api/v3/workspaces', v3WorkspacesRouter);
|
||||
|
||||
// api docs
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface ISecretBlindIndexData {
|
||||
export interface ISecretBlindIndexData extends Document {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
encryptedSaltCiphertext: string;
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface IWorkspace {
|
||||
slug: string;
|
||||
}>;
|
||||
autoCapitalization: boolean;
|
||||
isBlindedIndicesEnabled: boolean;
|
||||
}
|
||||
|
||||
const workspaceSchema = new Schema<IWorkspace>({
|
||||
@@ -21,10 +20,6 @@ const workspaceSchema = new Schema<IWorkspace>({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isBlindedIndicesEnabled: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Organization',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import secrets from './secrets';
|
||||
import workspaces from './workspaces';
|
||||
|
||||
export {
|
||||
secrets
|
||||
secrets,
|
||||
workspaces
|
||||
}
|
||||
@@ -23,6 +23,7 @@ router.get(
|
||||
query('workspaceId').exists().isString().trim(),
|
||||
query('environment').exists().isString().trim(),
|
||||
query('tagSlugs'),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
|
||||
80
backend/src/routes/v3/workspaces.ts
Normal file
80
backend/src/routes/v3/workspaces.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { workspacesController } from '../../controllers/v3';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
ADMIN,
|
||||
PERMISSION_READ_SECRETS
|
||||
} from '../../variables';
|
||||
import { param, body, validationResult } from 'express-validator';
|
||||
|
||||
// -- migration to blind indices endpoints
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/secrets/blind-index-status',
|
||||
param('workspaceId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
// requireAuth({
|
||||
// acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
// }),
|
||||
// requireWorkspaceAuth({
|
||||
// acceptedRoles: [ADMIN],
|
||||
// locationWorkspaceId: 'params',
|
||||
// }),
|
||||
workspacesController.getWorkspaceBlindIndexStatus
|
||||
);
|
||||
|
||||
router.get( // allow admins to get all workspace secrets (part of blind indices migration)
|
||||
'/:workspaceId/secrets',
|
||||
param('workspaceId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
locationWorkspaceId: 'params',
|
||||
}),
|
||||
workspacesController.getWorkspaceSecrets
|
||||
);
|
||||
|
||||
router.post( // allow admins to name all workspace secrets (part of blind indices migration)
|
||||
'/:workspaceId/secrets/names',
|
||||
param('workspaceId').exists().isString().trim(),
|
||||
body('secretsToUpdate')
|
||||
.exists()
|
||||
.isArray()
|
||||
.withMessage('secretsToUpdate must be an array')
|
||||
.customSanitizer((value) => {
|
||||
return value.map((secret: any) => ({
|
||||
secretName: secret.name,
|
||||
_id: secret._id
|
||||
}));
|
||||
}),
|
||||
body('secretsToUpdate.*.secretName')
|
||||
.exists()
|
||||
.isString()
|
||||
.withMessage('secretName must be a string'),
|
||||
body('secretsToUpdate.*._id')
|
||||
.exists()
|
||||
.isString()
|
||||
.withMessage('secretId must be a string'),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
workspacesController.nameWorkspaceSecrets
|
||||
);
|
||||
|
||||
// --
|
||||
|
||||
export default router;
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from '../interfaces/services/SecretService';
|
||||
import {
|
||||
createSecretBlindIndexDataHelper,
|
||||
getSecretBlindIndexSaltHelper,
|
||||
generateSecretBlindIndexWithSaltHelper,
|
||||
generateSecretBlindIndexHelper,
|
||||
createSecretHelper,
|
||||
getSecretsHelper,
|
||||
@@ -25,10 +27,11 @@ class SecretService {
|
||||
* Create secret blind index data containing encrypted blind index salt
|
||||
* for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Buffer} obj.salt - 16-byte random salt
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
*/
|
||||
static async createSecretBlindIndexData({
|
||||
workspaceId
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
@@ -37,6 +40,42 @@ class SecretService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secret blind index salt for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
||||
* @returns
|
||||
*/
|
||||
static async getSecretBlindIndexSalt({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await getSecretBlindIndexSaltHelper({
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate blind index for secret with name [secretName]
|
||||
* and salt [salt]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretName - name of secret to generate blind index for
|
||||
* @param {String} obj.salt - base64-salt
|
||||
*/
|
||||
static async generateSecretBlindIndexWithSalt({
|
||||
secretName,
|
||||
salt
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
}) {
|
||||
return await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return blind index for secret with
|
||||
* name [secretName] part of workspace with id [workspaceId]
|
||||
|
||||
Reference in New Issue
Block a user