mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 17:26:07 +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 secretsController from './secretsController';
|
||||||
|
import * as workspacesController from './workspacesController';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
secretsController
|
secretsController,
|
||||||
|
workspacesController
|
||||||
}
|
}
|
||||||
@@ -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]
|
* for workspace with id [workspaceId]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Types.ObjectId} obj.workspaceId
|
* @param {Types.ObjectId} obj.workspaceId
|
||||||
@@ -250,6 +250,64 @@ const createSecretBlindIndexDataHelper = async ({
|
|||||||
return secretBlindIndexData;
|
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]
|
* Generate blind index for secret with name [secretName]
|
||||||
* for workspace with id [workspaceId]
|
* for workspace with id [workspaceId]
|
||||||
@@ -280,16 +338,10 @@ const generateSecretBlindIndexHelper = async ({
|
|||||||
key: getEncryptionKey()
|
key: getEncryptionKey()
|
||||||
});
|
});
|
||||||
|
|
||||||
// generate secret blind index
|
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||||
const secretBlindIndex = (await argon2.hash(secretName, {
|
secretName,
|
||||||
type: argon2.argon2id,
|
salt
|
||||||
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;
|
return secretBlindIndex;
|
||||||
}
|
}
|
||||||
@@ -858,6 +910,8 @@ export {
|
|||||||
validateClientForSecret,
|
validateClientForSecret,
|
||||||
validateClientForSecrets,
|
validateClientForSecrets,
|
||||||
createSecretBlindIndexDataHelper,
|
createSecretBlindIndexDataHelper,
|
||||||
|
getSecretBlindIndexSaltHelper,
|
||||||
|
generateSecretBlindIndexWithSaltHelper,
|
||||||
generateSecretBlindIndexHelper,
|
generateSecretBlindIndexHelper,
|
||||||
createSecretHelper,
|
createSecretHelper,
|
||||||
getSecretsHelper,
|
getSecretsHelper,
|
||||||
|
|||||||
@@ -67,12 +67,16 @@ const validateClientForWorkspace = async ({
|
|||||||
message: 'Failed to find workspace'
|
message: 'Failed to find workspace'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (requireBlindIndicesEnabled && !workspace.isBlindedIndicesEnabled) {
|
if (requireBlindIndicesEnabled) {
|
||||||
// case: blind indices are not enabled for secrets in this workspace
|
// case: blind indices are not enabled for secrets in this workspace
|
||||||
// (i.e. workspace was created before blind indices were introduced
|
// (i.e. workspace was created before blind indices were introduced
|
||||||
// and no admin has enabled it)
|
// 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'
|
message: 'Failed workspace authorization due to blind indices not being enabled'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -148,8 +152,7 @@ const createWorkspace = async ({
|
|||||||
workspace = await new Workspace({
|
workspace = await new Workspace({
|
||||||
name,
|
name,
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
autoCapitalization: true,
|
autoCapitalization: true
|
||||||
isBlindedIndicesEnabled: true
|
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
// initialize bot for workspace
|
// initialize bot for workspace
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ import {
|
|||||||
tags as v2TagsRouter,
|
tags as v2TagsRouter,
|
||||||
} from './routes/v2';
|
} from './routes/v2';
|
||||||
import {
|
import {
|
||||||
secrets as v3SecretsRouter
|
secrets as v3SecretsRouter,
|
||||||
|
workspaces as v3WorkspacesRouter
|
||||||
} from './routes/v3';
|
} from './routes/v3';
|
||||||
import { healthCheck } from './routes/status';
|
import { healthCheck } from './routes/status';
|
||||||
import { getLogger } from './utils/logger';
|
import { getLogger } from './utils/logger';
|
||||||
@@ -159,6 +160,7 @@ const main = async () => {
|
|||||||
|
|
||||||
// v3 routes (experimental)
|
// v3 routes (experimental)
|
||||||
app.use('/api/v3/secrets', v3SecretsRouter);
|
app.use('/api/v3/secrets', v3SecretsRouter);
|
||||||
|
app.use('/api/v3/workspaces', v3WorkspacesRouter);
|
||||||
|
|
||||||
// api docs
|
// api docs
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Schema, model, Types, Document } from 'mongoose';
|
import { Schema, model, Types, Document } from 'mongoose';
|
||||||
|
|
||||||
export interface ISecretBlindIndexData {
|
export interface ISecretBlindIndexData extends Document {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
encryptedSaltCiphertext: string;
|
encryptedSaltCiphertext: string;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export interface IWorkspace {
|
|||||||
slug: string;
|
slug: string;
|
||||||
}>;
|
}>;
|
||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
isBlindedIndicesEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceSchema = new Schema<IWorkspace>({
|
const workspaceSchema = new Schema<IWorkspace>({
|
||||||
@@ -21,10 +20,6 @@ const workspaceSchema = new Schema<IWorkspace>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
isBlindedIndicesEnabled: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
organization: {
|
organization: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Organization',
|
ref: 'Organization',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import secrets from './secrets';
|
import secrets from './secrets';
|
||||||
|
import workspaces from './workspaces';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
secrets
|
secrets,
|
||||||
|
workspaces
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@ router.get(
|
|||||||
query('workspaceId').exists().isString().trim(),
|
query('workspaceId').exists().isString().trim(),
|
||||||
query('environment').exists().isString().trim(),
|
query('environment').exists().isString().trim(),
|
||||||
query('tagSlugs'),
|
query('tagSlugs'),
|
||||||
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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';
|
} from '../interfaces/services/SecretService';
|
||||||
import {
|
import {
|
||||||
createSecretBlindIndexDataHelper,
|
createSecretBlindIndexDataHelper,
|
||||||
|
getSecretBlindIndexSaltHelper,
|
||||||
|
generateSecretBlindIndexWithSaltHelper,
|
||||||
generateSecretBlindIndexHelper,
|
generateSecretBlindIndexHelper,
|
||||||
createSecretHelper,
|
createSecretHelper,
|
||||||
getSecretsHelper,
|
getSecretsHelper,
|
||||||
@@ -25,10 +27,11 @@ class SecretService {
|
|||||||
* 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]
|
* for workspace with id [workspaceId]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
|
* @param {Buffer} obj.salt - 16-byte random salt
|
||||||
* @param {Types.ObjectId} obj.workspaceId
|
* @param {Types.ObjectId} obj.workspaceId
|
||||||
*/
|
*/
|
||||||
static async createSecretBlindIndexData({
|
static async createSecretBlindIndexData({
|
||||||
workspaceId
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
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
|
* Create and return blind index for secret with
|
||||||
* name [secretName] part of workspace with id [workspaceId]
|
* name [secretName] part of workspace with id [workspaceId]
|
||||||
|
|||||||
Reference in New Issue
Block a user