From df83e8ceb9eeda5025e53bcc7fbecc8f45a36e95 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 17 Apr 2023 11:09:45 +0300 Subject: [PATCH] Complete first iteration of CRUD secrets operations by name --- backend/src/controllers/v1/botController.ts | 3 +- .../src/controllers/v2/secretsController.ts | 34 +- .../src/controllers/v3/secretsController.ts | 229 +++++-- .../src/ee/controllers/v1/secretController.ts | 2 +- .../ee/controllers/v1/workspaceController.ts | 2 +- backend/src/ee/helpers/log.ts | 1 + backend/src/ee/helpers/secret.ts | 6 +- backend/src/ee/models/secretVersion.ts | 9 +- backend/src/ee/services/EESecretService.ts | 2 +- backend/src/helpers/bot.ts | 2 +- backend/src/helpers/secret.ts | 21 +- backend/src/helpers/secrets.ts | 577 ++++++++++++++++-- backend/src/helpers/workspace.ts | 47 +- backend/src/interfaces/middleware/index.ts | 12 + .../services/SecretService/index.ts | 49 ++ backend/src/middleware/requireAuth.ts | 5 +- .../src/middleware/requireWorkspaceAuth.ts | 7 +- backend/src/models/secret.ts | 5 +- backend/src/models/secretBlindIndexData.ts | 14 +- backend/src/models/workspace.ts | 22 +- backend/src/routes/v3/secrets.ts | 81 ++- backend/src/services/SecretService.ts | 173 +++++- backend/src/services/TelemetryService.ts | 3 + backend/src/types/express/index.d.ts | 8 +- backend/src/utils/auth.ts | 26 + backend/src/utils/errors.ts | 10 + backend/src/variables/index.ts | 10 +- backend/src/variables/workspace.ts | 9 - .../ProjectEncryptionModeSection.tsx | 91 ++- 29 files changed, 1231 insertions(+), 229 deletions(-) create mode 100644 backend/src/interfaces/middleware/index.ts create mode 100644 backend/src/interfaces/services/SecretService/index.ts create mode 100644 backend/src/utils/auth.ts diff --git a/backend/src/controllers/v1/botController.ts b/backend/src/controllers/v1/botController.ts index ab86897cb..0fdce2cd9 100644 --- a/backend/src/controllers/v1/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; +import { Types } from 'mongoose'; import * as Sentry from '@sentry/node'; import { Bot, BotKey } from '../../models'; import { createBot } from '../../helpers/bot'; @@ -29,7 +30,7 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => { // -> create a new bot and return it bot = await createBot({ name: 'Infisical Bot', - workspaceId + workspaceId: new Types.ObjectId(workspaceId) }); } } catch (err) { diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 82b16edd6..c7d586d85 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -2,7 +2,7 @@ import to from 'await-to-js'; import { Types } from 'mongoose'; import { Request, Response } from 'express'; import { ISecret, Secret } from '../../models'; -import { IAction } from '../../ee/models'; +import { IAction, SecretVersion } from '../../ee/models'; import { SECRET_PERSONAL, SECRET_SHARED, @@ -33,6 +33,7 @@ import { * @param res */ export const batchSecrets = async (req: Request, res: Response) => { + const channel = getChannelFromUserAgent(req.headers['user-agent']); const postHogClient = TelemetryService.getPostHogClient(); @@ -146,7 +147,7 @@ export const batchSecrets = async (req: Request, res: Response) => { await Secret.bulkWrite(updateOperations); - const secretVersions = updateSecrets.map((u) => ({ + const secretVersions = updateSecrets.map((u) => new SecretVersion({ secret: new Types.ObjectId(u._id), version: listedSecretsObj[u._id.toString()].version, workspace: new Types.ObjectId(workspaceId), @@ -253,7 +254,7 @@ export const batchSecrets = async (req: Request, res: Response) => { // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId + workspaceId: new Types.ObjectId(workspaceId) }); const resObj: { [key: string]: ISecret[] | string[] } = {} @@ -422,13 +423,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - tags - }) => ({ - _id: new Types.ObjectId(), + secretValueTag + }) => new SecretVersion({ secret: _id, version, workspace, @@ -441,11 +437,7 @@ export const createSecrets = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - tags + secretValueTag })) }); @@ -471,7 +463,7 @@ export const createSecrets = async (req: Request, res: Response) => { // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId + workspaceId: new Types.ObjectId(workspaceId) }); const postHogClient = TelemetryService.getPostHogClient(); @@ -872,7 +864,7 @@ export const updateSecrets = async (req: Request, res: Response) => { // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId: key + workspaceId: new Types.ObjectId(key) }) const postHogClient = TelemetryService.getPostHogClient(); @@ -955,10 +947,6 @@ export const deleteSecrets = async (req: Request, res: Response) => { } */ - return res.status(200).send({ - message: 'delete secrets!!' - }); - const channel = getChannelFromUserAgent(req.headers['user-agent']) const toDelete = req.secrets.map((s: any) => s._id); @@ -1012,8 +1000,8 @@ export const deleteSecrets = async (req: Request, res: Response) => { // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId: key - }) + workspaceId: new Types.ObjectId(key) + }); const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index b10e98cbc..0df6b961c 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -1,14 +1,8 @@ import { Request, Response } from 'express'; import { Types } from 'mongoose'; -import { - Secret -} from '../../models'; -import crypto from 'crypto'; -import { SecretService } from '../../services'; - - -// TODO: modularize argon2id -import * as argon2 from 'argon2'; +import { SecretService, TelemetryService } from '../../services'; +import { getAuthDataPayloadIdObj } from '../../utils/auth'; +import { BadRequestError } from '../../utils/errors'; /** * Get secrets for workspace with id [workspaceId] and environment @@ -17,41 +11,92 @@ import * as argon2 from 'argon2'; * @param res */ export const getSecrets = async (req: Request, res: Response) => { - return res.status(200).send({ + const workspaceId = req.query.workspaceId as string; + const environment = req.query.environment as string; + const secrets = await SecretService.getSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + authData: req.authData + }); + + const postHogClient = TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pulled', + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: req.authData.authChannel, + userAgent: req.headers?.['user-agent'] + } + }); + } + + return res.status(200).send({ + secrets }); } /** - * Return secret with name [secretName] for workspace with id [workspaceId] - * and environment [environment] + * Get secret with name [secretName] * @param req * @param res */ export const getSecretByName = async (req: Request, res: Response) => { const { secretName } = req.params; const workspaceId = req.query.workspaceId as string; - const environment = req.query.workspaceId as string; + const environment = req.query.environment as string; + const type = req.query.type as 'shared' | 'personal' | undefined; + + const secret = await SecretService.getSecret({ + secretName, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + authData: req.authData + }); + + const postHogClient = TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pull', + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), + properties: { + numberOfSecrets: 1, + environment, + workspaceId, + channel: req.authData.authChannel, + userAgent: req.headers?.['user-agent'] + } + }); + } return res.status(200).send({ - + secret }); } /** - * Create secret for workspace with id [workspaceId] and - * environment [environment] + * Create secret with name [secretName] * @param req * @param res */ export const createSecret = async (req: Request, res: Response) => { - // TODO: the middleware should've prevalidated that the - // workspace with id [workspaceId] has disabled E2EE const { secretName } = req.params; const { workspaceId, environment, - value, type, secretKeyCiphertext, secretKeyIV, @@ -61,66 +106,140 @@ export const createSecret = async (req: Request, res: Response) => { secretValueTag } = req.body; - const secretBlindIndex = await SecretService.createSecretBlindIndex({ + const secret = await SecretService.createSecret({ secretName, - workspaceId: new Types.ObjectId(workspaceId) + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + authData: req.authData, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag }); - - // // use workspace salt - // const randomBytes = crypto.randomBytes(16); - - // // generate blind index - // // TODO 1: abstract away into create blind index function - // // TODO 2: create a get blind index function - // const secretBlindIndex = (await argon2.hash(secretName, { - // type: argon2.argon2id, - // salt: randomBytes, - // saltLength: 16, // default 16 bytes - // memoryCost: 65536, // default pool of 64 MiB per thread. - // hashLength: 32, - // parallelism: 1, - // raw: true - // })).toString('base64'); - - // const secret = await new Secret({ - // workspace: new Types.ObjectId(workspaceId), - // environment, - // type, - // secretBlindIndex, - // secretKeyCiphertext, - // secretKeyIV, - // secretKeyTag, - // secretValueCiphertext, - // secretValueIV, - // secretValueTag - // }).save(); + + const postHogClient = TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: 'secrets added', + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), + properties: { + numberOfSecrets: 1, + environment, + workspaceId, + channel: req.authData.authChannel, + userAgent: req.headers?.['user-agent'] + } + }); + } + + const secretWithoutBlindIndex = secret.toObject(); + delete secretWithoutBlindIndex.secretBlindIndex; return res.status(200).send({ - + secret: secretWithoutBlindIndex }); } /** - * Update secret with name [secretName] in workspace with id [workspaceId] + * Update secret with name [secretName] * @param req * @param res */ export const updateSecretByName = async (req: Request, res: Response) => { const { secretName } = req.params; + const { + workspaceId, + environment, + type, + secretValueCiphertext, + secretValueIV, + secretValueTag + } = req.body; + + const secret = await SecretService.updateSecret({ + secretName, + workspaceId, + environment, + type, + authData: req.authData, + secretValueCiphertext, + secretValueIV, + secretValueTag + }); + + + const postHogClient = TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: 'secrets modified', + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), + properties: { + numberOfSecrets: 1, + environment, + workspaceId, + channel: req.authData.authChannel, + userAgent: req.headers?.['user-agent'] + } + }); + } return res.status(200).send({ - + secret }); } /** - * Delete secret with name [secretName] in workspace with id [workspaceId] + * Delete secret with name [secretName] * @param req * @param res */ export const deleteSecretByName = async (req: Request, res: Response) => { + const { secretName } = req.params; + const { + workspaceId, + environment, + type + } = req.body; + const { secret, secrets } = await SecretService.deleteSecret({ + secretName, + workspaceId, + environment, + type, + authData: req.authData + }); + + const postHogClient = TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: 'secrets deleted', + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: req.authData.authChannel, + userAgent: req.headers?.['user-agent'] + } + }); + } + return res.status(200).send({ - + secret }); } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index e1aca670f..92503d772 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -207,7 +207,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { // take secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId: secret.workspace.toString() + workspaceId: secret.workspace }); } catch (err) { diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 5a18ae08e..3c4ad111d 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -304,7 +304,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons // take secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId + workspaceId: new Types.ObjectId(workspaceId) }); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts index 77d280ee1..077d5fe5a 100644 --- a/backend/src/ee/helpers/log.ts +++ b/backend/src/ee/helpers/log.ts @@ -4,6 +4,7 @@ import { Log, IAction } from '../models'; + /** * Create an (audit) log * @param {Object} obj diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index 7edee915f..a642859ef 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -2,7 +2,7 @@ import { Types } from 'mongoose'; import * as Sentry from '@sentry/node'; import { Secret, - ISecret + ISecret, } from '../../models'; import { SecretSnapshot, @@ -21,7 +21,7 @@ import { const takeSecretSnapshotHelper = async ({ workspaceId }: { - workspaceId: string; + workspaceId: Types.ObjectId; }) => { let secretSnapshot; @@ -143,7 +143,7 @@ const initSecretVersioningHelper = async () => { if (unversionedSecrets.length > 0) { await addSecretVersionsHelper({ - secretVersions: unversionedSecrets.map((s, idx) => ({ + secretVersions: unversionedSecrets.map((s, idx) => new SecretVersion({ ...s, secret: s._id, version: s.version ? s.version : 1, diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index 3095b52fb..63c7d936a 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -5,6 +5,7 @@ import { } from '../../variables'; export interface ISecretVersion { + _id: Types.ObjectId; secret: Types.ObjectId; version: number; workspace: Types.ObjectId; // new @@ -18,7 +19,6 @@ export interface ISecretVersion { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; - tags?: string[]; } const secretVersionSchema = new Schema( @@ -80,12 +80,7 @@ const secretVersionSchema = new Schema( secretValueTag: { type: String, // symmetric required: true - }, - tags: { - ref: 'Tag', - type: [Schema.Types.ObjectId], - default: [] - }, + } }, { timestamps: true diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts index cea614c96..e408e848e 100644 --- a/backend/src/ee/services/EESecretService.ts +++ b/backend/src/ee/services/EESecretService.ts @@ -25,7 +25,7 @@ class EESecretService { static async takeSecretSnapshot({ workspaceId }: { - workspaceId: string; + workspaceId: Types.ObjectId; }) { if (!EELicenseService.isLicenseValid) return; return await takeSecretSnapshotHelper({ workspaceId }); diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 1e242778d..7b7c773ea 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -112,7 +112,7 @@ const createBot = async ({ workspaceId, }: { name: string; - workspaceId: string; + workspaceId: Types.ObjectId; }) => { let bot; try { diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 7a7979357..d9e9f807b 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -10,7 +10,8 @@ import { EELogService } from '../ee/services'; import { - IAction + IAction, + SecretVersion } from '../ee/models'; import { SECRET_SHARED, @@ -189,8 +190,7 @@ const v1PushSecrets = async ({ secretKeyHash, }) => { const newSecret = newSecretsObj[`${type}-${secretKeyHash}`]; - return ({ - _id: new Types.ObjectId(), + return new SecretVersion({ secret: _id, version: version ? version + 1 : 1, workspace: new Types.ObjectId(workspaceId), @@ -261,8 +261,7 @@ const v1PushSecrets = async ({ secretValueIV, secretValueTag, secretValueHash - }) => ({ - _id: new Types.ObjectId(), + }) => new SecretVersion({ secret: _id, version, workspace, @@ -284,7 +283,7 @@ const v1PushSecrets = async ({ // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId + workspaceId: new Types.ObjectId(workspaceId) }); } catch (err) { Sentry.setUser(null); @@ -475,12 +474,12 @@ const v2PushSecrets = async ({ // (EE) add secret versions for new secrets EESecretService.addSecretVersions({ - secretVersions: newSecrets.map((secretDocument) => { - return { - ...secretDocument.toObject(), + secretVersions: newSecrets.map((secretDocument: ISecret) => { + return new SecretVersion({ + ...secretDocument, secret: secretDocument._id, isDeleted: false - } + }) }) }); @@ -495,7 +494,7 @@ const v2PushSecrets = async ({ // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ - workspaceId + workspaceId: new Types.ObjectId(workspaceId) }) // (EE) create (audit) log diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index f48855aef..fa8af5c19 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -1,4 +1,11 @@ import { Types } from 'mongoose'; +import { + CreateSecretParams, + GetSecretsParams, + GetSecretParams, + UpdateSecretParams, + DeleteSecretParams +} from '../interfaces/services/SecretService'; import { User, IUser, @@ -10,6 +17,10 @@ import { ISecret, SecretBlindIndexData, } from '../models'; +import { + IAction, + SecretVersion +} from '../ee/models'; import { validateMembership } from '../helpers/membership'; @@ -27,17 +38,32 @@ import { import { BadRequestError, UnauthorizedRequestError, - SecretNotFoundError + SecretNotFoundError, + SecretBlindIndexDataNotFoundError } from '../utils/errors'; import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY + AUTH_MODE_API_KEY, + SECRET_PERSONAL, + SECRET_SHARED, + ACTION_ADD_SECRETS, + ACTION_READ_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_DELETE_SECRETS } from '../variables'; import crypto from 'crypto'; import * as argon2 from 'argon2'; - +import { decryptSymmetric } from '../utils/crypto'; +import { getEncryptionKey } from '../config'; +import { + EESecretService, + EELogService +} from '../ee/services'; +import { + getAuthDataPayloadIdObj +} from '../utils/auth'; /** * Validate authenticated clients for secrets with id [secretId] based @@ -197,53 +223,13 @@ const validateClientForSecrets = async ({ } /** - * Create and return blind index for secret with - * name [name] part of workspace with id [workspaceId] + * Generate blind index for secret with name [secretName] + * for workspace with id [workspaceId] * @param {Object} obj * @param {Object} obj.secretName - name of secret to generate blind index for * @param {Object} obj.workspaceId - id of workspace that secret belongs to */ -const createSecretBlindIndexHelper = async ({ - secretName, - workspaceId -}: { - secretName: string; - workspaceId: Types.ObjectId; -}) => { - - // check if workspace blind index data exists - // const secretBlindIndexData = await SecretBlindIndexData.findOne({ - // workspace: workspaceId - // }); - - // if (!secretBlindIndexData) { - // // case: workspace blind index data has not been enabled - // } - - // TODO: randomBytes should come from the decrypted secretBlindIndexData - const randomBytes = crypto.randomBytes(16); - - const secretBlindIndex = (await argon2.hash(secretName, { - type: argon2.argon2id, - salt: randomBytes, - 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 the blind index for the secret with - * name [name] part of workspace with id [workspaceId] - * @param {Object} obj - * @param {Object} obj.secretName - name of secret to generate blind index for - * @param {Object} obj.workspaceId - id of workspace that secret belongs to - */ -const getSecretBlindIndexHelper = async ({ +const generateSecretBlindIndexHelper = async ({ secretName, workspaceId }: { @@ -256,14 +242,505 @@ const getSecretBlindIndexHelper = async ({ workspace: workspaceId }); - if (!secretBlindIndexData) { - // case: workspace blind index data has not been enabled + if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); + + // decrypt workspace salt + const salt = decryptSymmetric({ + ciphertext: secretBlindIndexData.encryptedSaltCiphertext, + iv: secretBlindIndexData.saltIV, + tag: secretBlindIndexData.saltTag, + 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'); + + return secretBlindIndex; +} + +/** + * Create secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to create + * @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for + * @param {String} obj.environment - environment in workspace to create secret for + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ +const createSecretHelper = async ({ + secretName, + workspaceId, + environment, + type, + authData, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag +}: CreateSecretParams) => { + // preliminary OK + // pending check for other types of clients + + const secretBlindIndex = await generateSecretBlindIndexHelper({ + secretName, + workspaceId: new Types.ObjectId(workspaceId) + }); + + const exists = await Secret.exists({ + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + type + }); + + if (exists) throw BadRequestError({ + message: 'Failed to create secret that already exists' + }); + + if (type === SECRET_PERSONAL) { + // case: secret type is personal -> check if a corresponding shared secret + // with the same blind index [secretBlindIndex] exists + + const exists = await Secret.exists({ + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + type: SECRET_SHARED + }); + + if (!exists) throw BadRequestError({ + message: 'Failed to create personal secret override for no corresponding shared secret' + }); + + // TODO: adapt to other client types + + if (!(authData.authPayload instanceof User)) throw BadRequestError({ + message: 'Failed to create personal secret override for no specified user' + }); } + + // create secret + const secret = await new Secret({ + version: 1, + workspace: new Types.ObjectId(workspaceId), + environment, + type, + ...(type === SECRET_PERSONAL && (authData.authPayload instanceof User) ? { + user: authData.authPayload._id + } : {}), + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag + }).save(); + + const secretVersion = new SecretVersion({ + secret: secret._id, + version: secret.version, + workspace: secret.workspace, + type, + ...(type === SECRET_PERSONAL && authData.authPayload instanceof User ? { + user: authData.authPayload._id + } : {}), + environment: secret.environment, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag + }); + + // // (EE) add version for new secret + await EESecretService.addSecretVersions({ + secretVersions: [secretVersion] + }); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_ADD_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: [secret._id] + }); + + action && await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP + }); + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }); + + return secret; +} + +/** + * Get secrets for workspace with id [workspaceId] and environment [environment] + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace + * @param {String} obj.environment - environment in workspace + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ +const getSecretsHelper = async ({ + workspaceId, + environment, + authData +}: GetSecretsParams) => { + // preliminary OK + // pending check for other types of clients + + let secrets: ISecret[] = []; + + if (authData.authPayload instanceof User) { + // case: get personal secrets first + secrets = await Secret.find({ + workspace: new Types.ObjectId(workspaceId), + environment, + type: SECRET_PERSONAL, + user: authData.authPayload._id + }); + } + + secrets = secrets.concat(await Secret.find({ + workspace: new Types.ObjectId(workspaceId), + environment, + type: SECRET_SHARED, + secretBlindIndex: { + $nin: secrets.map((secret) => secret.secretBlindIndex) + } + })); + + const action = await EELogService.createAction({ + name: ACTION_READ_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: secrets.map((secret) => secret._id) + }); + + action && await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP + }); + + return secrets; +} + +/** + * Get secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to get + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ +const getSecretHelper = async ({ + secretName, + workspaceId, + environment, + type, + authData +}: GetSecretParams) => { + // preliminary OK + // pending check for other types of clients + + const secretBlindIndex = await generateSecretBlindIndexHelper({ + secretName, + workspaceId: new Types.ObjectId(workspaceId) + }); + + let secret; + + if (authData.authPayload instanceof User) { + // case: find any personal secret matching criteria + secret = await Secret.findOne({ + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + environment, + type: type ?? SECRET_PERSONAL, + ...(type === SECRET_PERSONAL ? { + user: authData.authPayload._id + } : {}) + }); + + if (!secret) { + // case: failed to find personal secret matching criteria + // -> find shared secret matching criteria + secret = await Secret.findOne({ + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + environment, + type: SECRET_SHARED + }); + } + } + + if (!secret) throw SecretNotFoundError(); + + const action = await EELogService.createAction({ + name: ACTION_READ_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: [secret._id] + }); + + action && await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP + }); + + return secret; +} + +/** + * Update secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to update + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {String} obj.secretValueCiphertext - ciphertext of secret value + * @param {String} obj.secretValueIV - IV of secret value + * @param {String} obj.secretValueTag - tag of secret value + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ +const updateSecretHelper = async ({ + secretName, + workspaceId, + environment, + type, + authData, + secretValueCiphertext, + secretValueIV, + secretValueTag +}: UpdateSecretParams) => { + // preliminary OK + // pending check for other types of clients + const secretBlindIndex = await generateSecretBlindIndexHelper({ + secretName, + workspaceId: new Types.ObjectId(workspaceId) + }); + + let secret: ISecret | null = null; + + if (type === SECRET_SHARED) { + secret = await Secret.findOneAndUpdate( + { + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + environment, + type + }, + { + secretValueCiphertext, + secretValueIV, + secretValueTag, + $inc: { version: 1 } + }, + { + new: true + } + ); + } else if (type === SECRET_PERSONAL && authData.authPayload instanceof User) { + secret = await Secret.findOneAndUpdate( + { + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + environment, + type, + user: authData.authPayload._id + }, + { + secretValueCiphertext, + secretValueIV, + secretValueTag, + $inc: { version: 1 } + }, + { + new: true + } + ); + } + + if (!secret) throw SecretNotFoundError(); + + const secretVersion = new SecretVersion({ + secret: secret._id, + version: secret.version, + workspace: secret.workspace, + type, + ...(type === SECRET_PERSONAL && authData.authPayload instanceof User ? { + user: authData.authPayload._id + } : {}), + environment: secret.environment, + isDeleted: false, + secretKeyCiphertext: secret.secretKeyCiphertext, + secretKeyIV: secret.secretKeyIV, + secretKeyTag: secret.secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag + }); + + // // (EE) add version for new secret + await EESecretService.addSecretVersions({ + secretVersions: [secretVersion] + }); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_UPDATE_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: [secret._id] + }); + + action && await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP + }); + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }); + + return secret; +} + +/** + * Delete secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to delete + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ +const deleteSecretHelper = async ({ + secretName, + workspaceId, + environment, + type, + authData +}: DeleteSecretParams) => { + // preliminary OK + // pending check for other types of clients + + const secretBlindIndex = await generateSecretBlindIndexHelper({ + secretName, + workspaceId: new Types.ObjectId(workspaceId) + }); + + let secrets: ISecret[] = []; + let secret: ISecret | null = null; + + if (type === SECRET_SHARED) { + secrets = await Secret.find({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment + }); + + secret = await Secret.findOneAndDelete({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type + }); + + await Secret.deleteMany({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment + }); + } else if (type === SECRET_PERSONAL && authData.authPayload instanceof User) { + secret = await Secret.findOneAndDelete({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + user: authData.authPayload._id + }); + + if (secret) { + secrets = [secret]; + } + } + + if (!secret) throw SecretNotFoundError(); + + await EESecretService.markDeletedSecretVersions({ + secretIds: secrets.map((secret) => secret._id) + }); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_DELETE_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: secrets.map((secret) => secret._id) + }); + + // (EE) take a secret snapshot + action && await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP + }); + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }); + + return ({ + secrets, + secret + }); } export { validateClientForSecret, validateClientForSecrets, - createSecretBlindIndexHelper, - getSecretBlindIndexHelper + generateSecretBlindIndexHelper, + createSecretHelper, + getSecretsHelper, + getSecretHelper, + updateSecretHelper, + deleteSecretHelper } \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index f3b27bf10..220102eff 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; import { Types } from 'mongoose'; import { Workspace, @@ -13,6 +14,7 @@ import { IServiceAccount, ServiceTokenData, IServiceTokenData, + SecretBlindIndexData } from '../models'; import { createBot } from '../helpers/bot'; import { validateUserClientForWorkspace } from '../helpers/user'; @@ -26,6 +28,8 @@ import { AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY } from '../variables'; +import { getEncryptionKey } from '../config'; +import { encryptSymmetric } from '../utils/crypto'; /** * Validate authenticated clients for workspace with id [workspaceId] based @@ -42,7 +46,8 @@ const validateClientForWorkspace = async ({ workspaceId, environment, acceptedRoles, - requiredPermissions + requiredPermissions, + requireBlindIndicesEnabled }: { authData: { authMode: string; @@ -52,6 +57,7 @@ const validateClientForWorkspace = async ({ environment?: string; acceptedRoles: Array<'admin' | 'member'>; requiredPermissions?: string[]; + requireBlindIndicesEnabled: boolean; }) => { const workspace = await Workspace.findById(workspaceId); @@ -60,6 +66,16 @@ const validateClientForWorkspace = async ({ message: 'Failed to find workspace' }); + if (requireBlindIndicesEnabled && !workspace.isBlindedIndicesEnabled) { + // 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({ + message: 'Failed workspace authorization due to blind indices not being enabled' + }); + } + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { const membership = await validateUserClientForWorkspace({ user: authData.authPayload, @@ -130,13 +146,36 @@ const createWorkspace = async ({ // create workspace workspace = await new Workspace({ name, - organization: organizationId + organization: organizationId, + autoCapitalization: true, + isBlindedIndicesEnabled: true }).save(); - const bot = await createBot({ + // initialize bot for workspace + await createBot({ name: 'Infisical Bot', - workspaceId: workspace._id.toString() + workspaceId: workspace._id }); + + // initialize blind index salt for workspace + const salt = crypto.randomBytes(16).toString('base64'); + + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag + } = encryptSymmetric({ + plaintext: salt, + key: getEncryptionKey() + }); + + await new SecretBlindIndexData({ + workspace: workspace._id, + encryptedSaltCiphertext, + saltIV, + saltTag + }).save(); + } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts new file mode 100644 index 000000000..4b54399ce --- /dev/null +++ b/backend/src/interfaces/middleware/index.ts @@ -0,0 +1,12 @@ +import { + IUser, + IServiceAccount, + IServiceTokenData +} from '../../models'; + +export interface AuthData { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + authChannel: string; + authIP: string; +} \ No newline at end of file diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts new file mode 100644 index 000000000..64da16beb --- /dev/null +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -0,0 +1,49 @@ +import { Types } from 'mongoose'; +import { AuthData } from '../../middleware'; + +export interface CreateSecretParams { + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + type: 'shared' | 'personal'; + authData: AuthData; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; +} + +export interface GetSecretsParams { + workspaceId: Types.ObjectId; + environment: string; + authData: AuthData; +} + +export interface GetSecretParams { + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + type?: 'shared' | 'personal'; + authData: AuthData; +} + +export interface UpdateSecretParams { + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + type: 'shared' | 'personal', + authData: AuthData + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; +} + +export interface DeleteSecretParams { + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + type: 'shared' | 'personal'; + authData: AuthData; +} \ No newline at end of file diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 16191d607..508248b19 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -21,6 +21,7 @@ import { AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY } from '../variables'; +import { getChannelFromUserAgent } from '../utils/posthog'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -88,7 +89,9 @@ const requireAuth = ({ req.authData = { authMode, - authPayload // User, ServiceAccount, ServiceTokenData + authPayload, // User, ServiceAccount, ServiceTokenData + authChannel: getChannelFromUserAgent(req.headers['user-agent']), + authIP: req.ip } return next(); diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 5d094f972..76f723df2 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -17,12 +17,14 @@ const requireWorkspaceAuth = ({ acceptedRoles, locationWorkspaceId, locationEnvironment = undefined, - requiredPermissions = [] + requiredPermissions = [], + requireBlindIndicesEnabled = false }: { acceptedRoles: Array<'admin' | 'member'>; locationWorkspaceId: req; locationEnvironment?: req | undefined; requiredPermissions?: string[]; + requireBlindIndicesEnabled?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { const workspaceId = req[locationWorkspaceId]?.workspaceId; @@ -34,7 +36,8 @@ const requireWorkspaceAuth = ({ workspaceId: new Types.ObjectId(workspaceId), environment, acceptedRoles, - requiredPermissions + requiredPermissions, + requireBlindIndicesEnabled }); if (membership) { diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index d3184b5e1..725741ccf 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Schema, model, Types } from 'mongoose'; import { SECRET_SHARED, SECRET_PERSONAL, @@ -59,7 +59,8 @@ const secretSchema = new Schema( required: true }, secretBlindIndex: { - type: String + type: String, + select: false }, secretKeyCiphertext: { type: String, diff --git a/backend/src/models/secretBlindIndexData.ts b/backend/src/models/secretBlindIndexData.ts index 9d0842ba4..aeaa3f027 100644 --- a/backend/src/models/secretBlindIndexData.ts +++ b/backend/src/models/secretBlindIndexData.ts @@ -3,7 +3,9 @@ import { Schema, model, Types, Document } from 'mongoose'; export interface ISecretBlindIndexData { _id: Types.ObjectId; workspace: Types.ObjectId; - encryptedSalt: string; + encryptedSaltCiphertext: string; + saltIV: string; + saltTag: string; } const secretBlindIndexDataSchema = new Schema( @@ -13,7 +15,15 @@ const secretBlindIndexDataSchema = new Schema( ref: 'Workspace', required: true }, - encryptedSalt: { + encryptedSaltCiphertext: { + type: String, + required: true + }, + saltIV: { + type: String, + required: true + }, + saltTag: { type: String, required: true } diff --git a/backend/src/models/workspace.ts b/backend/src/models/workspace.ts index e6f1a69d3..c31542b3a 100644 --- a/backend/src/models/workspace.ts +++ b/backend/src/models/workspace.ts @@ -1,20 +1,15 @@ import { Schema, model, Types } from 'mongoose'; -import { - WORKSPACE_ENCRYPTION_MODE_E2EE, - WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE, - WORKSPACE_ENCRYPTION_MODE_NOT_E2EE -} from '../variables'; export interface IWorkspace { _id: Types.ObjectId; name: string; - encryptionMode: string; organization: Types.ObjectId; environments: Array<{ name: string; slug: string; }>; autoCapitalization: boolean; + isBlindedIndicesEnabled: boolean; } const workspaceSchema = new Schema({ @@ -22,19 +17,14 @@ const workspaceSchema = new Schema({ type: String, required: true }, - encryptionMode: { - type: String, - default: 'e2ee', - enum: [ - WORKSPACE_ENCRYPTION_MODE_E2EE, - WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE, - WORKSPACE_ENCRYPTION_MODE_NOT_E2EE - ] - }, autoCapitalization: { type: Boolean, default: true, }, + isBlindedIndicesEnabled: { + type: Boolean, + required: true + }, organization: { type: Schema.Types.ObjectId, ref: 'Organization', @@ -70,4 +60,4 @@ const workspaceSchema = new Schema({ const Workspace = model('Workspace', workspaceSchema); -export default Workspace; +export default Workspace; \ No newline at end of file diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 92bc2d9a3..f40c0f332 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -1,18 +1,38 @@ import express from 'express'; const router = express.Router(); import { - requireAuth, validateRequest + requireAuth, + requireWorkspaceAuth, + validateRequest } from '../../middleware'; import { body, param, query } from 'express-validator'; import { secretsController } from '../../controllers/v3'; - -// note: future endpoints pending brainstorm + implementation +import { + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + ADMIN, + MEMBER, + PERMISSION_WRITE_SECRETS, + SECRET_SHARED, + SECRET_PERSONAL, + PERMISSION_READ_SECRETS +} from '../../variables'; router.get( '/', query('workspaceId').exists().isString().trim(), query('environment').exists().isString().trim(), query('tagSlugs'), + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'query', + locationEnvironment: 'query', + requiredPermissions: [PERMISSION_READ_SECRETS], + requireBlindIndicesEnabled: true, + }), secretsController.getSecrets ); @@ -20,6 +40,7 @@ router.post( '/:secretName', body('workspaceId').exists().isString().trim(), body('environment').exists().isString().trim(), + body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), body('secretKeyCiphertext').exists().isString().trim(), body('secretKeyIV').exists().isString().trim(), body('secretKeyTag').exists().isString().trim(), @@ -27,6 +48,16 @@ router.post( body('secretValueIV').exists().isString().trim(), body('secretValueTag').exists().isString().trim(), validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + }), secretsController.createSecret ); @@ -35,23 +66,61 @@ router.get( param('secretName').exists().isString().trim(), query('workspaceId').exists().isString().trim(), query('environment').exists().isString().trim(), + query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'query', + locationEnvironment: 'query', + requiredPermissions: [PERMISSION_READ_SECRETS], + requireBlindIndicesEnabled: true, + }), secretsController.getSecretByName ); router.patch( '/:secretName', param('secretName').exists().isString().trim(), - query('workspaceId').exists().isString().trim(), - query('environment').exists().isString().trim(), + body('workspaceId').exists().isString().trim(), + body('environment').exists().isString().trim(), + body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), + body('secretValueCiphertext').exists().isString().trim(), + body('secretValueIV').exists().isString().trim(), + body('secretValueTag').exists().isString().trim(), validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + }), secretsController.updateSecretByName ); router.delete( '/:secretName', param('secretName').exists().isString().trim(), - query('workspaceId').exists().isString().trim(), + body('workspaceId').exists().isString().trim(), + body('environment').exists().isString().trim(), + body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + }), secretsController.deleteSecretByName ); diff --git a/backend/src/services/SecretService.ts b/backend/src/services/SecretService.ts index 0c7132327..0275b2cfb 100644 --- a/backend/src/services/SecretService.ts +++ b/backend/src/services/SecretService.ts @@ -1,48 +1,187 @@ // WIP import { Types } from 'mongoose'; +import { + ISecret +} from '../models'; +import { + CreateSecretParams, + GetSecretsParams, + GetSecretParams, + UpdateSecretParams, + DeleteSecretParams +} from '../interfaces/services/SecretService'; import { - createSecretBlindIndexHelper, - getSecretBlindIndexHelper + generateSecretBlindIndexHelper, + createSecretHelper, + getSecretsHelper, + getSecretHelper, + updateSecretHelper, + deleteSecretHelper } from '../helpers/secrets'; class SecretService { /** * Create and return blind index for secret with - * name [name] part of workspace with id [workspaceId] + * name [secretName] part of workspace with id [workspaceId] * @param {Object} obj * @param {Object} obj.secretName - name of secret to generate blind index for * @param {Object} obj.workspaceId - id of workspace that secret belongs to */ - static async createSecretBlindIndex({ + static async generateSecretBlindIndex({ secretName, workspaceId, }: { secretName: string; workspaceId: Types.ObjectId; }) { - return await createSecretBlindIndexHelper({ + return await generateSecretBlindIndexHelper({ secretName, workspaceId }); } + + /** + * Create secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to create + * @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for + * @param {String} obj.environment - environment in workspace to create secret for + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async createSecret({ + secretName, + workspaceId, + environment, + type, + authData, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag + }: CreateSecretParams) { + return await createSecretHelper({ + secretName, + workspaceId, + environment, + type, + authData, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag + }); + } /** - * Return the blind index for the secret with - * name [name] part of workspace with id [workspaceId] + * Get secrets for workspace with id [workspaceId] and environment [environment] * @param {Object} obj - * @param {Object} obj.secretName - name of secret to generate blind index for - * @param {Object} obj.workspaceId - id of workspace that secret belongs to + * @param {Types.ObjectId} obj.workspaceId - id of workspace + * @param {String} obj.environment - environment in workspace + * @param {AuthData} obj.authData - authentication data on request + * @returns */ - static async getSecretBlindIndex({ + static async getSecrets({ + workspaceId, + environment, + authData + }: GetSecretsParams) { + return await getSecretsHelper({ + workspaceId, + environment, + authData + }); + } + + /** + * Get secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to get + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async getSecret({ secretName, - workspaceId - }: { - secretName: string; - workspaceId: Types.ObjectId; - }) { - return await getSecretBlindIndexHelper({ + workspaceId, + environment, + type, + authData + }: GetSecretParams) { + return await getSecretHelper({ secretName, - workspaceId + workspaceId, + environment, + type, + authData + }); + } + + /** + * Update secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to update + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {String} obj.secretValueCiphertext - ciphertext of secret value + * @param {String} obj.secretValueIV - IV of secret value + * @param {String} obj.secretValueTag - tag of secret value + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async updateSecret({ + secretName, + workspaceId, + environment, + type, + secretValueCiphertext, + secretValueIV, + secretValueTag, + authData + }: UpdateSecretParams) { + return await updateSecretHelper({ + secretName, + workspaceId, + environment, + type, + authData, + secretValueCiphertext, + secretValueIV, + secretValueTag + }); + } + + /** + * Delete secret with name [secretName] + * @param {Object} obj + * @param {String} obj.secretName - name of secret to delete + * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to + * @param {String} obj.environment - environment in workspace that secret belongs to + * @param {'shared' | 'personal'} obj.type - type of secret + * @param {AuthData} obj.authData - authentication data on request + * @returns + */ + static async deleteSecret({ + secretName, + workspaceId, + environment, + type, + authData + }: DeleteSecretParams) { + return await deleteSecretHelper({ + secretName, + workspaceId, + environment, + type, + authData }); } } diff --git a/backend/src/services/TelemetryService.ts b/backend/src/services/TelemetryService.ts index 248ad3633..6e35a403f 100644 --- a/backend/src/services/TelemetryService.ts +++ b/backend/src/services/TelemetryService.ts @@ -60,6 +60,9 @@ class Telemetry { serviceAccount?: IServiceAccount; serviceTokenData?: any; // TODO: fix (it's ServiceTokenData with user populated) }) => { + + // TODO: modify to accept authData instead + let distinctId = ''; if (user) { diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index bff5e96c3..6d2acd606 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -5,6 +5,9 @@ import { IServiceTokenData, ISecret } from '../../models'; +import { + AuthData +} from '../../interfaces/middleware'; // TODO: fix (any) types declare global { @@ -29,10 +32,7 @@ declare global { serviceTokenData: any; apiKeyData: any; query?: any; - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; requestData: { [key: string]: string }; diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts new file mode 100644 index 000000000..ce2961d70 --- /dev/null +++ b/backend/src/utils/auth.ts @@ -0,0 +1,26 @@ +import { AuthData } from '../interfaces/middleware'; +import { + User, + ServiceAccount, + ServiceTokenData +} from '../models'; + +const getAuthDataPayloadIdObj = (authData: AuthData) => { + if (authData.authPayload instanceof User) { + return { userId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceAccount) { + return { serviceAccountId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceTokenData) { + return { serviceTokenDataId: authData.authPayload._id }; + } + + return {}; +}; + +export { + getAuthDataPayloadIdObj +} \ No newline at end of file diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 987215091..4b698bee3 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -153,6 +153,16 @@ export const SecretNotFoundError = (error?: Partial) => new stack: error?.stack }); +//* ----->[SECRET BLIND INDEX DATA ERRORS]<----- +export const SecretBlindIndexDataNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'secret_blind_index_data_not_found_error', + message: error?.message ?? 'The requested secret was not found', + context: error?.context, + stack: error?.stack +}); + //* ----->[SECRET SNAPSHOT ERRORS]<----- export const SecretSnapshotNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 92f76ac8e..979de21ab 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -77,11 +77,6 @@ import { AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY } from './authentication'; -import { - WORKSPACE_ENCRYPTION_MODE_E2EE, - WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE, - WORKSPACE_ENCRYPTION_MODE_NOT_E2EE -} from './workspace'; export { OWNER, @@ -153,8 +148,5 @@ export { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY, - WORKSPACE_ENCRYPTION_MODE_E2EE, - WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE, - WORKSPACE_ENCRYPTION_MODE_NOT_E2EE + AUTH_MODE_API_KEY }; diff --git a/backend/src/variables/workspace.ts b/backend/src/variables/workspace.ts index c0be34fcb..e69de29bb 100644 --- a/backend/src/variables/workspace.ts +++ b/backend/src/variables/workspace.ts @@ -1,9 +0,0 @@ -const WORKSPACE_ENCRYPTION_MODE_E2EE = 'e2ee'; -const WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE = 'blind-indexed-e2ee'; -const WORKSPACE_ENCRYPTION_MODE_NOT_E2EE = 'not-e2ee'; - -export { - WORKSPACE_ENCRYPTION_MODE_E2EE, - WORKSPACE_ENCRYPTION_MODE_BLIND_INDEXED_E2EE, - WORKSPACE_ENCRYPTION_MODE_NOT_E2EE -} \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectEncryptionModeSection/ProjectEncryptionModeSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectEncryptionModeSection/ProjectEncryptionModeSection.tsx index 94c1f2037..47630effb 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectEncryptionModeSection/ProjectEncryptionModeSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectEncryptionModeSection/ProjectEncryptionModeSection.tsx @@ -1,8 +1,93 @@ +import { useEffect } from 'react'; +import { + Controller, + useForm +} from 'react-hook-form'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; + +import { + Button, + FormControl, + Select, + SelectItem +} from '@app/components/v2'; + +// TODO: modify in accordance with what was discussed with +// Maidul + +// Do with select for now them replace. + +const items = [ + { value: 'e2ee', label: 'E2EE' }, + { value: 'blind-indexed-e2ee', label: 'Blind Indexed E2EE' } +]; + +const formSchema = yup.object({ + mode: yup.string().required().label('Project Mode') +}); + +type FormData = yup.InferType; export const ProjectEncryptionModeSection = () => { + const { + handleSubmit, + control, + reset, + formState: { isDirty, isSubmitting } + } = useForm({ resolver: yupResolver(formSchema) }); + + useEffect(() => { + reset({ mode: 'blind-indexed-e2ee' }); + }, []); + + const onFormSubmit = async ({ mode }: FormData) => { + console.log('onFormSubmit'); + console.log('mode: ', mode); + }; + return ( -
- Project encryption mode section -
+
+

Encryption Mode

+
+ { + console.log('field: ', field); + return ( + + + + ); + }} + control={control} + name="mode" + /> +
+ +
); } \ No newline at end of file