Add workspaces v3 endpoints for blind-index naming/labeling

This commit is contained in:
Tuan Dang
2023-04-17 23:48:48 +03:00
parent 763ec1aa0f
commit b62ea41e02
11 changed files with 297 additions and 25 deletions

View File

@@ -1,5 +1,7 @@
import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController';
export {
secretsController
secretsController,
workspacesController
}

View 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
});
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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))

View File

@@ -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;

View File

@@ -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',

View File

@@ -1,5 +1,7 @@
import secrets from './secrets';
import workspaces from './workspaces';
export {
secrets
secrets,
workspaces
}

View File

@@ -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]
}),

View 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;

View File

@@ -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]