From 87e047a1520ae56ee2210c77cbfba50bf36a6631 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 6 May 2023 22:07:59 +0300 Subject: [PATCH] Checkpoint finish preliminary support for ROOT_ENCRYPTION_KEY --- backend/package-lock.json | 48 +-- backend/package.json | 2 +- backend/src/config/index.ts | 22 +- .../v1/integrationAuthController.ts | 6 +- .../src/controllers/v1/signupController.ts | 2 +- .../src/controllers/v2/secretsController.ts | 22 +- .../src/ee/controllers/v1/secretController.ts | 8 +- backend/src/ee/models/secretVersion.ts | 20 +- backend/src/helpers/auth.ts | 1 - backend/src/helpers/bot.ts | 210 +++++------- backend/src/helpers/integration.ts | 115 +------ backend/src/helpers/membership.ts | 98 +----- backend/src/helpers/membershipOrg.ts | 86 +---- backend/src/helpers/organization.ts | 106 +----- backend/src/helpers/secret.ts | 14 + backend/src/helpers/secrets.ts | 295 +++++----------- backend/src/helpers/user.ts | 226 +----------- backend/src/helpers/workspace.ts | 124 +------ backend/src/index.ts | 17 +- backend/src/middleware/requireAuth.ts | 4 +- backend/src/middleware/requireBotAuth.ts | 5 +- .../src/middleware/requireIntegrationAuth.ts | 6 +- .../requireIntegrationAuthorizationAuth.ts | 6 +- .../src/middleware/requireMembershipAuth.ts | 9 +- .../middleware/requireMembershipOrgAuth.ts | 12 +- .../src/middleware/requireOrganizationAuth.ts | 5 +- backend/src/middleware/requireSecretAuth.ts | 9 +- backend/src/middleware/requireSecretsAuth.ts | 4 +- .../middleware/requireServiceAccountAuth.ts | 11 +- .../middleware/requireServiceTokenDataAuth.ts | 5 +- .../src/middleware/requireWorkspaceAuth.ts | 2 +- backend/src/models/bot.ts | 6 +- backend/src/models/secretBlindIndexData.ts | 8 +- backend/src/routes/v2/secrets.ts | 2 +- backend/src/utils/crypto/index.ts | 73 +--- backend/src/utils/setup/backfill.ts | 215 ------------ backend/src/utils/setup/backfillData.ts | 324 ++++++++++++++++++ backend/src/utils/setup/index.ts | 48 ++- backend/src/utils/setup/reencryptData.ts | 126 +++++++ backend/src/utils/setup/validateConfig.ts | 69 ++++ backend/src/validation/bot.ts | 98 ++++++ backend/src/validation/config.ts | 21 -- backend/src/validation/index.ts | 11 +- backend/src/validation/integration.ts | 103 ++++++ .../integrationAuth.ts | 4 +- backend/src/validation/membership.ts | 94 +++++ backend/src/validation/membershipOrg.ts | 93 +++++ backend/src/validation/organization.ts | 104 ++++++ backend/src/validation/secrets.ts | 174 ++++++++++ .../{helpers => validation}/serviceAccount.ts | 23 +- .../serviceTokenData.ts | 16 +- backend/src/validation/user.ts | 209 +++++++++++ backend/src/validation/workspace.ts | 124 +++++++ frontend/src/pages/signup.tsx | 1 - 54 files changed, 1877 insertions(+), 1569 deletions(-) delete mode 100644 backend/src/utils/setup/backfill.ts create mode 100644 backend/src/utils/setup/backfillData.ts create mode 100644 backend/src/utils/setup/reencryptData.ts create mode 100644 backend/src/utils/setup/validateConfig.ts create mode 100644 backend/src/validation/bot.ts delete mode 100644 backend/src/validation/config.ts create mode 100644 backend/src/validation/integration.ts rename backend/src/{helpers => validation}/integrationAuth.ts (95%) create mode 100644 backend/src/validation/membership.ts create mode 100644 backend/src/validation/membershipOrg.ts create mode 100644 backend/src/validation/organization.ts create mode 100644 backend/src/validation/secrets.ts rename backend/src/{helpers => validation}/serviceAccount.ts (92%) rename backend/src/{helpers => validation}/serviceTokenData.ts (92%) create mode 100644 backend/src/validation/user.ts create mode 100644 backend/src/validation/workspace.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index ba7fc017b..9f149c29b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -33,7 +33,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", - "infisical-node": "^1.1.3", + "infisical-node": "^1.2.1", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", @@ -5331,14 +5331,6 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6904,13 +6896,12 @@ } }, "node_modules/infisical-node": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz", - "integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.2.1.tgz", + "integrity": "sha512-zEB0w5+1O0mv9qc68bq4f9jDjrtwdbqjJebnwodgy8U1XZElDXeMDQgSMCtgYan7JRmVlH6s/LM8X7kUF+67ZA==", "dependencies": { "axios": "^1.3.3", "dotenv": "^16.0.3", - "node-cache": "^5.1.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } @@ -8404,17 +8395,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node_modules/node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -17266,11 +17246,6 @@ "wrap-ansi": "^7.0.0" } }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -18461,13 +18436,12 @@ "dev": true }, "infisical-node": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz", - "integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.2.1.tgz", + "integrity": "sha512-zEB0w5+1O0mv9qc68bq4f9jDjrtwdbqjJebnwodgy8U1XZElDXeMDQgSMCtgYan7JRmVlH6s/LM8X7kUF+67ZA==", "requires": { "axios": "^1.3.3", "dotenv": "^16.0.3", - "node-cache": "^5.1.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } @@ -19615,14 +19589,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "requires": { - "clone": "2.x" - } - }, "node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", diff --git a/backend/package.json b/backend/package.json index 2c1154adb..c3a173a7b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,7 +24,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", - "infisical-node": "^1.1.3", + "infisical-node": "^1.2.1", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 326597db3..b61db8811 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,24 +1,22 @@ import InfisicalClient from 'infisical-node'; -import { validateEncryptionKey } from '../validation'; -const client = new InfisicalClient({ +export const client = new InfisicalClient({ token: process.env.INFISICAL_TOKEN! }); export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000; export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue == undefined ? false : (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue; -export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue; // TODO: deprecate in favor of INFISICAL_ENCRYPTION_KEY -export const getRootEncryptionKey = async (): Promise => { - const encryptionKey = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue; - if (encryptionKey) { - // validate [encryptionKey] to make sure it is in base64 format and 256-bit - validateEncryptionKey(encryptionKey); - return encryptionKey; - } - - return encryptionKey; +export const getEncryptionKey = async () => { + const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue; + return secretValue === '' ? undefined : secretValue; } + +export const getRootEncryptionKey = async () => { + const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue; + return secretValue === '' ? undefined : secretValue; +} + export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10; export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d'; export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue; diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index b21a0cd42..a472598d1 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -5,7 +5,7 @@ import { IntegrationAuth, Bot } from '../../models'; -import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables'; +import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables'; import { IntegrationService } from '../../services'; import { getApps, @@ -129,7 +129,9 @@ export const saveIntegrationAccessToken = async ( integration }, { workspace: new Types.ObjectId(workspaceId), - integration + integration, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }, { new: true, upsert: true diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 193699c15..b035eb3e8 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -47,7 +47,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => { error: 'Failed to send email verification code' }); } - + return res.status(200).send({ message: `Sent an email verification code to ${email}` }); diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 51c93182b..a3773f815 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -9,7 +9,9 @@ import { ACTION_ADD_SECRETS, ACTION_READ_SECRETS, ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS + ACTION_DELETE_SECRETS, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8 } from '../../variables'; import { UnauthorizedRequestError, ValidationError } from '../../utils/errors'; import { EventService } from '../../services'; @@ -81,7 +83,9 @@ export const batchSecrets = async (req: Request, res: Response) => { workspace: new Types.ObjectId(workspaceId), path: fullFolderPath, folder: folderId, - secretBlindIndex + secretBlindIndex, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }); break; case 'PATCH': @@ -96,6 +100,8 @@ export const batchSecrets = async (req: Request, res: Response) => { secretBlindIndex, folder: folderId, path: fullFolderPath, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }); break; case 'DELETE': @@ -196,6 +202,8 @@ export const batchSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: u.secretCommentCiphertext, secretCommentIV: u.secretCommentIV, secretCommentTag: u.secretCommentTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, tags: u.tags })); @@ -444,6 +452,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, tags }); }) @@ -490,7 +500,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag + secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 })) }); @@ -831,6 +843,8 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, tags, ...(( secretCommentCiphertext !== undefined && @@ -884,6 +898,8 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, tags: tags ? tags : secret.tags }); }) diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index cfdd93cf3..8e3b39b54 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -162,6 +162,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + algorithm, + keyEncoding } = oldSecretVersion; // update secret @@ -182,6 +184,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + algorithm, + keyEncoding }, { new: true @@ -205,7 +209,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag + secretValueTag, + algorithm, + keyEncoding }).save(); // take secret snapshot diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index a430834a9..9aca6af1c 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -2,6 +2,9 @@ import { Schema, model, Types } from 'mongoose'; import { SECRET_SHARED, SECRET_PERSONAL, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 } from '../../variables'; export interface ISecretVersion { @@ -20,6 +23,8 @@ export interface ISecretVersion { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; + algorithm: 'aes-256-gcm'; + keyEncoding: 'utf8' | 'base64'; } const secretVersionSchema = new Schema( @@ -85,7 +90,20 @@ const secretVersionSchema = new Schema( secretValueTag: { type: String, // symmetric required: true - } + }, + algorithm: { // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true + }, + keyEncoding: { + type: String, + enum: [ + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 + ], + required: true + }, }, { timestamps: true diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index a52abb709..977a82586 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -41,7 +41,6 @@ const validateAuthMode = ({ headers: { [key: string]: string | string[] | undefined }, acceptedAuthModes: string[] }) => { - // TODO: refactor middleware const apiKey = headers['x-api-key']; const authHeader = headers['authorization']; diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index fe7aba31f..04dfd7e36 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -4,12 +4,7 @@ import { BotKey, Secret, ISecret, - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData, + IUser } from "../models"; import { generateKeyPair, @@ -19,91 +14,16 @@ import { } from '../utils/crypto'; import { SECRET_SHARED, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 } from "../variables"; -import { getEncryptionKey } from "../config"; -import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors"; -import { validateUserClientForWorkspace } from "../helpers/user"; -import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount"; - -/** - * Validate authenticated clients for bot with id [botId] based - * on any known permissions. - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId} obj.botId - id of bot to validate against - * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles - */ -const validateClientForBot = async ({ - authData, - botId, - acceptedRoles, -}: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - botId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; -}) => { - const bot = await Bot.findById(botId); - - if (!bot) throw BotNotFoundError(); - - if ( - authData.authMode === AUTH_MODE_JWT && - authData.authPayload instanceof User - ) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: bot.workspace, - acceptedRoles, - }); - - return bot; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && - authData.authPayload instanceof ServiceAccount - ) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: bot.workspace, - }); - - return bot; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_TOKEN && - authData.authPayload instanceof ServiceTokenData - ) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for bot", - }); - } - - if ( - authData.authMode === AUTH_MODE_API_KEY && - authData.authPayload instanceof User - ) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: bot.workspace, - acceptedRoles, - }); - - return bot; - } - - throw BotNotFoundError({ - message: "Failed client authorization for bot", - }); -}; +import { + getEncryptionKey, + getRootEncryptionKey, + client +} from "../config"; +import { InternalServerError } from "../utils/errors"; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -118,23 +38,52 @@ const createBot = async ({ name: string; workspaceId: Types.ObjectId; }) => { + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + const { publicKey, privateKey } = generateKeyPair(); - const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ - plaintext: privateKey, - key: await getEncryptionKey(), + + if (rootEncryptionKey) { + const { + ciphertext, + iv, + tag + } = client.encryptSymmetric(privateKey, rootEncryptionKey); + + return await new Bot({ + name, + workspace: workspaceId, + isActive: false, + publicKey, + encryptedPrivateKey: ciphertext, + iv, + tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64 + }).save(); + + } else if (encryptionKey) { + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: privateKey, + key: await getEncryptionKey(), + }); + + return await new Bot({ + name, + workspace: workspaceId, + isActive: false, + publicKey, + encryptedPrivateKey: ciphertext, + iv, + tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }).save(); + } + + throw InternalServerError({ + message: 'Failed to create new bot due to missing encryption key' }); - - const bot = await new Bot({ - name, - workspace: workspaceId, - isActive: false, - publicKey, - encryptedPrivateKey: ciphertext, - iv, - tag, - }).save(); - - return bot; }; /** @@ -188,34 +137,54 @@ const getSecretsHelper = async ({ * @returns {String} key - decrypted workspace key */ const getKey = async ({ workspaceId }: { workspaceId: string }) => { + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + const botKey = await BotKey.findOne({ workspace: workspaceId, - }).populate<{ sender: IUser }>("sender", "publicKey"); + }) + .populate<{ sender: IUser }>("sender", "publicKey"); if (!botKey) throw new Error("Failed to find bot key"); const bot = await Bot.findOne({ workspace: workspaceId, - }).select("+encryptedPrivateKey +iv +tag"); + }).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding"); if (!bot) throw new Error("Failed to find bot"); if (!bot.isActive) throw new Error("Bot is not active"); - const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: bot.encryptedPrivateKey, - iv: bot.iv, - tag: bot.tag, - key: await getEncryptionKey(), - }); + if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) { + // case: encoding scheme is base64 + const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag); - const key = decryptAsymmetric({ - ciphertext: botKey.encryptedKey, - nonce: botKey.nonce, - publicKey: botKey.sender.publicKey as string, - privateKey: privateKeyBot, - }); + return decryptAsymmetric({ + ciphertext: botKey.encryptedKey, + nonce: botKey.nonce, + publicKey: botKey.sender.publicKey as string, + privateKey: privateKeyBot, + }); + } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { + + // case: encoding scheme is utf8 + const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: bot.encryptedPrivateKey, + iv: bot.iv, + tag: bot.tag, + key: encryptionKey + }); + + return decryptAsymmetric({ + ciphertext: botKey.encryptedKey, + nonce: botKey.nonce, + publicKey: botKey.sender.publicKey as string, + privateKey: privateKeyBot, + }); + } - return key; + throw InternalServerError({ + message: "Failed to obtain bot's copy of workspace key needed for bot operations" + }); }; /** @@ -276,7 +245,6 @@ const decryptSymmetricHelper = async ({ }; export { - validateClientForBot, createBot, getSecretsHelper, encryptSymmetricHelper, diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 46eb22766..4d26bded9 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -3,40 +3,20 @@ import { Types } from 'mongoose'; import { Bot, Integration, - IntegrationAuth, - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData + IntegrationAuth } from '../models'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { BotService } from '../services'; import { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY, INTEGRATION_VERCEL, - INTEGRATION_NETLIFY + INTEGRATION_NETLIFY, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8 } from '../variables'; import { UnauthorizedRequestError, - IntegrationAuthNotFoundError, - IntegrationNotFoundError } from '../utils/errors'; import RequestError from '../utils/requestError'; -import { - validateClientForIntegrationAuth -} from '../helpers/integrationAuth'; -import { - validateUserClientForWorkspace -} from '../helpers/user'; -import { - validateServiceAccountClientForWorkspace -} from '../helpers/serviceAccount'; -import { IntegrationService } from '../services'; interface Update { workspace: string; @@ -45,84 +25,6 @@ interface Update { accountId?: string; } -/** - * Validate authenticated clients for integration with id [integrationId] based - * on any known permissions. - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId} obj.integrationId - id of integration to validate against - * @param {String} obj.environment - (optional) environment in workspace to validate against - * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint - */ - const validateClientForIntegration = async ({ - authData, - integrationId, - acceptedRoles -}: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - integrationId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; -}) => { - - const integration = await Integration.findById(integrationId); - if (!integration) throw IntegrationNotFoundError(); - - const integrationAuth = await IntegrationAuth - .findById(integration.integrationAuth) - .select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); - - if (!integrationAuth) throw IntegrationAuthNotFoundError(); - - const accessToken = (await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id - })).accessToken; - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: integration.workspace, - acceptedRoles - }); - - return ({ integration, accessToken }); - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: integration.workspace - }); - - return ({ integration, accessToken }); - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: 'Failed service token authorization for integration' - }); - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: integration.workspace, - acceptedRoles - }); - - return ({ integration, accessToken }); - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for integration' - }); -} - /** * Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration * named [integration] @@ -400,7 +302,9 @@ const setIntegrationAuthRefreshHelper = async ({ }, { refreshCiphertext: obj.ciphertext, refreshIV: obj.iv, - refreshTag: obj.tag + refreshTag: obj.tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }, { new: true }); @@ -461,7 +365,9 @@ const setIntegrationAuthAccessHelper = async ({ accessCiphertext: encryptedAccessTokenObj.ciphertext, accessIV: encryptedAccessTokenObj.iv, accessTag: encryptedAccessTokenObj.tag, - accessExpiresAt + accessExpiresAt, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }, { new: true }); @@ -475,7 +381,6 @@ const setIntegrationAuthAccessHelper = async ({ } export { - validateClientForIntegration, handleOAuthExchangeHelper, syncIntegrationsHelper, getIntegrationAuthRefreshHelper, diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 503ca9fc6..a78100248 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -2,105 +2,12 @@ import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Membership, - Key, - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData + Key } from '../models'; import { MembershipNotFoundError, - BadRequestError, - UnauthorizedRequestError + BadRequestError } from '../utils/errors'; -import { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; -import { - validateUserClientForWorkspace -} from '../helpers/user'; -import { - validateServiceAccountClientForWorkspace -} from '../helpers/serviceAccount'; -import { - validateServiceTokenDataClientForWorkspace -} from '../helpers/serviceTokenData'; - -/** - * Validate authenticated clients for membership with id [membershipId] based - * on any known permissions. - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId} obj.membershipId - id of membership to validate against - * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles - * @returns {Membership} - validated membership - */ -const validateClientForMembership = async ({ - authData, - membershipId, - acceptedRoles -}: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - membershipId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; -}) => { - - const membership = await Membership.findById(membershipId); - - if (!membership) throw MembershipNotFoundError({ - message: 'Failed to find membership' - }); - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: membership.workspace, - acceptedRoles - }); - - return membership; - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: membership.workspace - }); - - return membership; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload, - workspaceId: new Types.ObjectId(membership.workspace) - }); - - return membership; - } - - if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: membership.workspace, - acceptedRoles - }); - - return membership; - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for membership' - }); -} /** * Validate that user with id [userId] is a member of workspace with id [workspaceId] @@ -230,7 +137,6 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { }; export { - validateClientForMembership, validateMembership, addMemberships, findMembership, diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index d8b944145..b5f4bb366 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -3,95 +3,12 @@ import { MembershipOrg, Workspace, Membership, - Key, - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData + Key } from '../models'; import { MembershipOrgNotFoundError, - BadRequestError, UnauthorizedRequestError } from '../utils/errors'; -import { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; - -/** - * Validate authenticated clients for organization membership with id [membershipOrgId] based - * on any known permissions. - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against - * @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles - * @param {MembershipOrg} - validated organization membership - */ -const validateClientForMembershipOrg = async ({ - authData, - membershipOrgId, - acceptedRoles, - acceptedStatuses -}: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - membershipOrgId: Types.ObjectId; - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; -}) => { - const membershipOrg = await MembershipOrg.findById(membershipOrgId); - - if (!membershipOrg) throw MembershipOrgNotFoundError({ - message: 'Failed to find organization membership ' - }); - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateMembershipOrg({ - userId: authData.authPayload._id, - organizationId: membershipOrg.organization, - acceptedRoles, - acceptedStatuses - }); - - return membershipOrg; - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({ - message: 'Failed service account client authorization for organization membership' - }); - - return membershipOrg; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: 'Failed service account client authorization for organization membership' - }); - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateMembershipOrg({ - userId: authData.authPayload._id, - organizationId: membershipOrg.organization, - acceptedRoles, - acceptedStatuses - }); - - return membershipOrg; - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for organization membership' - }); -} /** * Validate that user with id [userId] is a member of organization with id [organizationId] @@ -234,7 +151,6 @@ const deleteMembershipOrg = async ({ }; export { - validateClientForMembershipOrg, validateMembershipOrg, findMembershipOrg, addMembershipsOrg, diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 9e67ebb00..b9fe4eb9c 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -1,21 +1,8 @@ import Stripe from "stripe"; import { Types } from "mongoose"; -import { - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData, -} from "../models"; import { Organization, MembershipOrg } from "../models"; import { - ACCEPTED, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY, - OWNER, + ACCEPTED } from "../variables"; import { getStripeSecretKey, @@ -23,94 +10,6 @@ import { getStripeProductTeam, getStripeProductStarter, } from "../config"; -import { - UnauthorizedRequestError, - OrganizationNotFoundError, -} from "../utils/errors"; -import { validateUserClientForOrganization } from "../helpers/user"; -import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount"; - -/** - * Validate accepted clients for organization with id [organizationId] - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId} obj.organizationId - id of organization to validate against - */ -const validateClientForOrganization = async ({ - authData, - organizationId, - acceptedRoles, - acceptedStatuses, -}: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - organizationId: Types.ObjectId; - acceptedRoles: Array<"owner" | "admin" | "member">; - acceptedStatuses: Array<"invited" | "accepted">; -}) => { - const organization = await Organization.findById(organizationId); - - if (!organization) { - throw OrganizationNotFoundError({ - message: "Failed to find organization", - }); - } - - if ( - authData.authMode === AUTH_MODE_JWT && - authData.authPayload instanceof User - ) { - const membershipOrg = await validateUserClientForOrganization({ - user: authData.authPayload, - organization, - acceptedRoles, - acceptedStatuses, - }); - - return { organization, membershipOrg }; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && - authData.authPayload instanceof ServiceAccount - ) { - await validateServiceAccountClientForOrganization({ - serviceAccount: authData.authPayload, - organization, - }); - - return { organization }; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_TOKEN && - authData.authPayload instanceof ServiceTokenData - ) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for organization", - }); - } - - if ( - authData.authMode === AUTH_MODE_API_KEY && - authData.authPayload instanceof User - ) { - const membershipOrg = await validateUserClientForOrganization({ - user: authData.authPayload, - organization, - acceptedRoles, - acceptedStatuses, - }); - - return { organization, membershipOrg }; - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for organization", - }); -}; /** * Create an organization with name [name] @@ -258,8 +157,7 @@ const updateSubscriptionOrgQuantity = async ({ }; export { - validateClientForOrganization, createOrganization, initSubscriptionOrg, - updateSubscriptionOrgQuantity, + updateSubscriptionOrgQuantity }; diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index e75f46e0f..9b81d79c9 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -9,6 +9,8 @@ import { ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, ACTION_READ_SECRETS, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, } from "../variables"; import _ from "lodash"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; @@ -194,6 +196,8 @@ const v1PushSecrets = async ({ secretValueIV: newSecret.ivValue, secretValueTag: newSecret.tagValue, secretValueHash: newSecret.hashValue, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }); }), }); @@ -225,6 +229,8 @@ const v1PushSecrets = async ({ secretCommentIV: s.ivComment, secretCommentTag: s.tagComment, secretCommentHash: s.hashComment, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }; if (toAdd[idx].type === "personal") { @@ -254,6 +260,8 @@ const v1PushSecrets = async ({ secretValueIV, secretValueTag, secretValueHash, + algorithm, + keyEncoding }) => new SecretVersion({ secret: _id, @@ -271,6 +279,8 @@ const v1PushSecrets = async ({ secretValueIV, secretValueTag, secretValueHash, + algorithm, + keyEncoding }) ), }); @@ -467,6 +477,8 @@ const v2PushSecrets = async ({ workspace: workspaceId, type: toAdd[idx].type, environment, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, ...(toAdd[idx].type === "personal" ? { user: userId } : {}), })) ); @@ -478,6 +490,8 @@ const v2PushSecrets = async ({ ...secretDocument, secret: secretDocument._id, isDeleted: false, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }); }), }); diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index fe39859d8..562c173fa 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -7,50 +7,27 @@ import { DeleteSecretParams } from '../interfaces/services/SecretService'; import { - AuthData -} from '../interfaces/middleware'; -import { - User, - Workspace, - ServiceAccount, - ServiceTokenData, Secret, ISecret, SecretBlindIndexData, } from '../models'; import { SecretVersion } from '../ee/models'; -import { - validateMembership -} from '../helpers/membership'; -import { - validateUserClientForSecret, - validateUserClientForSecrets -} from '../helpers/user'; -import { - validateServiceTokenDataClientForSecrets, - validateServiceTokenDataClientForWorkspace -} from '../helpers/serviceTokenData'; -import { - validateServiceAccountClientForSecrets, - validateServiceAccountClientForWorkspace -} from '../helpers/serviceAccount'; import { BadRequestError, - UnauthorizedRequestError, SecretNotFoundError, - SecretBlindIndexDataNotFoundError + SecretBlindIndexDataNotFoundError, + InternalServerError } from '../utils/errors'; import { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY, SECRET_PERSONAL, SECRET_SHARED, ACTION_ADD_SECRETS, ACTION_READ_SECRETS, ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS + ACTION_DELETE_SECRETS, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 } from '../variables'; import crypto from 'crypto'; import * as argon2 from 'argon2'; @@ -58,7 +35,7 @@ import { encryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8 } from '../utils/crypto'; -import { getEncryptionKey } from '../config'; +import { getEncryptionKey, client, getRootEncryptionKey } from '../config'; import { TelemetryService } from '../services'; import { EESecretService, @@ -69,157 +46,6 @@ import { getAuthDataPayloadUserObj } from '../utils/auth'; -/** - * Validate authenticated clients for secrets with id [secretId] based - * on any known permissions. - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId} obj.secretId - id of secret to validate against - * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint - */ -const validateClientForSecret = async ({ - authData, - secretId, - acceptedRoles, - requiredPermissions -}: { - authData: AuthData; - secretId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; - requiredPermissions: string[]; -}) => { - const secret = await Secret.findById(secretId); - - if (!secret) throw SecretNotFoundError({ - message: 'Failed to find secret' - }); - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForSecret({ - user: authData.authPayload, - secret, - acceptedRoles, - requiredPermissions - }); - - return secret; - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: secret.workspace, - environment: secret.environment, - requiredPermissions - }); - - return secret; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload, - workspaceId: secret.workspace, - environment: secret.environment - }); - - return secret; - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForSecret({ - user: authData.authPayload, - secret, - acceptedRoles, - requiredPermissions - }); - - return secret; - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for secret' - }); -} - -/** - * Validate authenticated clients for secrets with ids [secretIds] based - * on any known permissions. - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against - * @param {String} obj.environment - (optional) environment in workspace to validate against - * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint - */ -const validateClientForSecrets = async ({ - authData, - secretIds, - requiredPermissions -}: { - authData: AuthData; - secretIds: Types.ObjectId[]; - requiredPermissions: string[]; -}) => { - - let secrets: ISecret[] = []; - - secrets = await Secret.find({ - _id: { - $in: secretIds - } - }); - - if (secrets.length != secretIds.length) { - throw BadRequestError({ message: 'Failed to validate non-existent secrets' }) - } - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForSecrets({ - user: authData.authPayload, - secrets, - requiredPermissions - }); - - return secrets; - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForSecrets({ - serviceAccount: authData.authPayload, - secrets, - requiredPermissions - }); - - return secrets; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForSecrets({ - serviceTokenData: authData.authPayload, - secrets, - requiredPermissions - }); - - return secrets; - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForSecrets({ - user: authData.authPayload, - secrets, - requiredPermissions - }); - - return secrets; - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for secrets resource' - }); -} - /** * Create secret blind index data containing encrypted blind index [salt] * for workspace with id [workspaceId] @@ -231,26 +57,47 @@ const createSecretBlindIndexDataHelper = async ({ }: { workspaceId: Types.ObjectId; }) => { + // initialize random blind index salt for workspace const salt = crypto.randomBytes(16).toString('base64'); - - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = encryptSymmetric128BitHexKeyUTF8({ - plaintext: salt, - key: await getEncryptionKey() - }); - - const secretBlindIndexData = await new SecretBlindIndexData({ - workspace: workspaceId, - encryptedSaltCiphertext, - saltIV, - saltTag - }).save(); - - return secretBlindIndexData; + + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + if (rootEncryptionKey) { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag + } = client.encryptSymmetric(salt, rootEncryptionKey); + + return await new SecretBlindIndexData({ + workspace: workspaceId, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64 + }).save(); + } else { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag + } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: salt, + key: encryptionKey + }); + + return await new SecretBlindIndexData({ + workspace: workspaceId, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }).save(); + } } /** @@ -264,22 +111,36 @@ const getSecretBlindIndexSaltHelper = async ({ }: { workspaceId: Types.ObjectId; }) => { - // check if workspace blind index data exists + + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + const secretBlindIndexData = await SecretBlindIndexData.findOne({ workspace: workspaceId - }); + }).select('+algorithm +keyEncoding'); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); - - // decrypt workspace salt - const salt = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secretBlindIndexData.encryptedSaltCiphertext, - iv: secretBlindIndexData.saltIV, - tag: secretBlindIndexData.saltTag, - key: await getEncryptionKey() + + if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) { + return client.decryptSymmetric( + secretBlindIndexData.encryptedSaltCiphertext, + rootEncryptionKey, + secretBlindIndexData.saltIV, + secretBlindIndexData.saltTag + ); + } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) { + // decrypt workspace salt + return decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secretBlindIndexData.encryptedSaltCiphertext, + iv: secretBlindIndexData.saltIV, + tag: secretBlindIndexData.saltTag, + key: encryptionKey + }); + } + + throw InternalServerError({ + message: 'Failed to obtain workspace salt needed for secret blind indexing' }); - - return salt; } /** @@ -422,7 +283,9 @@ const createSecretHelper = async ({ secretValueTag, secretCommentCiphertext, secretCommentIV, - secretCommentTag + secretCommentTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }).save(); const secretVersion = new SecretVersion({ @@ -439,7 +302,9 @@ const createSecretHelper = async ({ secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag + secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }); // // (EE) add version for new secret @@ -729,7 +594,9 @@ const updateSecretHelper = async ({ secretKeyTag: secret.secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag + secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 }); // (EE) add version for new secret @@ -890,8 +757,6 @@ const deleteSecretHelper = async ({ } export { - validateClientForSecret, - validateClientForSecrets, createSecretBlindIndexDataHelper, getSecretBlindIndexSaltHelper, generateSecretBlindIndexWithSaltHelper, diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 73b87f5d6..a69b72695 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,24 +1,8 @@ -import { Types } from 'mongoose'; import { IUser, - ISecret, - IServiceAccount, User, - Membership, - IOrganization, - Organization, } from '../models'; import { sendMail } from './nodemailer'; -import { validateMembership } from './membership'; -import _ from 'lodash'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; -import { - validateMembershipOrg -} from '../helpers/membershipOrg'; -import { - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS -} from '../variables'; /** * Initialize a user under email [email] @@ -26,7 +10,7 @@ import { * @param {String} obj.email - email of user to initialize * @returns {Object} user - the initialized user */ -const setupAccount = async ({ email }: { email: string }) => { +export const setupAccount = async ({ email }: { email: string }) => { const user = await new User({ email }).save(); @@ -52,7 +36,7 @@ const setupAccount = async ({ email }: { email: string }) => { * @param {String} obj.verifier - verifier for auth SRP * @returns {Object} user - the completed user */ -const completeAccount = async ({ +export const completeAccount = async ({ userId, firstName, lastName, @@ -113,7 +97,7 @@ const completeAccount = async ({ * @param {String} obj.ip - login ip address * @param {String} obj.userAgent - login user-agent */ -const checkUserDevice = async ({ +export const checkUserDevice = async ({ user, ip, userAgent @@ -148,206 +132,4 @@ const checkUserDevice = async ({ } }); } -} - -/** - * Validate that user (client) can access workspace - * with id [workspaceId] and its environment [environment] with required permissions - * [requiredPermissions] - * @param {Object} obj - * @param {User} obj.user - user client - * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against - * @param {String} environment - (optional) environment in workspace to validate against - * @param {String[]} requiredPermissions - required permissions as part of the endpoint - */ -const validateUserClientForWorkspace = async ({ - user, - workspaceId, - environment, - acceptedRoles, - requiredPermissions -}: { - user: IUser; - workspaceId: Types.ObjectId; - environment?: string; - acceptedRoles: Array<'admin' | 'member'>; - requiredPermissions?: string[]; -}) => { - - // validate user membership in workspace - const membership = await validateMembership({ - userId: user._id, - workspaceId, - acceptedRoles - }); - - let runningIsDisallowed = false; - requiredPermissions?.forEach((requiredPermission: string) => { - switch (requiredPermission) { - case PERMISSION_READ_SECRETS: - runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); - break; - case PERMISSION_WRITE_SECRETS: - runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); - break; - default: - break; - } - - if (runningIsDisallowed) { - throw UnauthorizedRequestError({ - message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` - }); - } - }); - - return membership; -} - -/** - * Validate that user (client) can access secret [secret] - * with required permissions [requiredPermissions] - * @param {Object} obj - * @param {User} obj.user - user client - * @param {Secret[]} obj.secrets - secrets to validate against - * @param {String[]} requiredPermissions - required permissions as part of the endpoint - */ -const validateUserClientForSecret = async ({ - user, - secret, - acceptedRoles, - requiredPermissions -}: { - user: IUser; - secret: ISecret; - acceptedRoles?: Array<'admin' | 'member'>; - requiredPermissions?: string[]; -}) => { - const membership = await validateMembership({ - userId: user._id, - workspaceId: secret.workspace, - acceptedRoles - }); - - if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { - const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); - - if (isDisallowed) { - throw UnauthorizedRequestError({ - message: 'You do not have the required permissions to perform this action' - }); - } - } -} - -/** - * Validate that user (client) can access secrets [secrets] - * with required permissions [requiredPermissions] - * @param {Object} obj - * @param {User} obj.user - user client - * @param {Secret[]} obj.secrets - secrets to validate against - * @param {String[]} requiredPermissions - required permissions as part of the endpoint - */ - const validateUserClientForSecrets = async ({ - user, - secrets, - requiredPermissions -}: { - user: IUser; - secrets: ISecret[]; - requiredPermissions?: string[]; -}) => { - - // TODO: add acceptedRoles? - - const userMemberships = await Membership.find({ user: user._id }) - const userMembershipById = _.keyBy(userMemberships, 'workspace'); - const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); - - // for each secret check if the secret belongs to a workspace the user is a member of - secrets.forEach((secret: ISecret) => { - if (!workspaceIdsSet.has(secret.workspace.toString())) { - throw BadRequestError({ - message: 'Failed authorization for the secret' - }); - } - - if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { - const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; - const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); - - if (isDisallowed) { - throw UnauthorizedRequestError({ - message: 'You do not have the required permissions to perform this action' - }); - } - } - }); -} - -/** - * Validate that user (client) can access service account [serviceAccount] - * with required permissions [requiredPermissions] - * @param {Object} obj - * @param {User} obj.user - user client - * @param {ServiceAccount} obj.serviceAccount - service account to validate against - * @param {String[]} requiredPermissions - required permissions as part of the endpoint - */ -const validateUserClientForServiceAccount = async ({ - user, - serviceAccount, - requiredPermissions -}: { - user: IUser; - serviceAccount: IServiceAccount; - requiredPermissions?: string[]; -}) => { - if (!serviceAccount.user.equals(user._id)) { - // case: user who created service account is not the - // same user that is on the request - await validateMembershipOrg({ - userId: user._id, - organizationId: serviceAccount.organization, - acceptedRoles: [], - acceptedStatuses: [] - }); - } -} - -/** - * Validate that user (client) can access organization [organization] - * @param {Object} obj - * @param {User} obj.user - user client - * @param {Organization} obj.organization - organization to validate against - */ - const validateUserClientForOrganization = async ({ - user, - organization, - acceptedRoles, - acceptedStatuses -}: { - user: IUser; - organization: IOrganization; - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; -}) => { - const membershipOrg = await validateMembershipOrg({ - userId: user._id, - organizationId: organization._id, - acceptedRoles, - acceptedStatuses - }); - - return membershipOrg; -} - -export { - setupAccount, - completeAccount, - checkUserDevice, - validateUserClientForWorkspace, - validateUserClientForSecrets, - validateUserClientForServiceAccount, - validateUserClientForOrganization, - validateUserClientForSecret -}; +} \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 1b6e7737e..6bc880981 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,135 +1,14 @@ import * as Sentry from '@sentry/node'; -import crypto from 'crypto'; -import { Types } from 'mongoose'; import { Workspace, Bot, Membership, Key, - Secret, - User, - IUser, - ServiceAccountWorkspacePermission, - ServiceAccount, - IServiceAccount, - ServiceTokenData, - IServiceTokenData, - SecretBlindIndexData + Secret } from '../models'; import { createBot } from '../helpers/bot'; -import { validateUserClientForWorkspace } from '../helpers/user'; -import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; -import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData'; -import { validateMembership } from '../helpers/membership'; -import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors'; -import { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; import { SecretService } from '../services'; -/** - * Validate authenticated clients for workspace with id [workspaceId] based - * on any known permissions. - * @param {Object} obj - * @param {Object} obj.authData - authenticated client details - * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against - * @param {String} obj.environment - (optional) environment in workspace to validate against - * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint - */ -const validateClientForWorkspace = async ({ - authData, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, - requireBlindIndicesEnabled -}: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - workspaceId: Types.ObjectId; - environment?: string; - acceptedRoles: Array<'admin' | 'member'>; - requiredPermissions?: string[]; - requireBlindIndicesEnabled: boolean; -}) => { - - const workspace = await Workspace.findById(workspaceId); - - if (!workspace) throw WorkspaceNotFoundError({ - message: 'Failed to find workspace' - }); - - 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) - - 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' - }); - } - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - const membership = await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId, - environment, - acceptedRoles, - requiredPermissions - }); - - return ({ membership }); - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId, - environment, - requiredPermissions - }); - - return {}; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload, - workspaceId, - environment, - requiredPermissions - }); - - return {}; - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - const membership = await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId, - environment, - acceptedRoles, - requiredPermissions - }); - - return ({ membership }); - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for workspace' - }); -} - /** * Create a workspace with name [name] in organization with id [organizationId] * and a bot for it. @@ -202,7 +81,6 @@ const deleteWorkspace = async ({ id }: { id: string }) => { }; export { - validateClientForWorkspace, createWorkspace, deleteWorkspace }; diff --git a/backend/src/index.ts b/backend/src/index.ts index e15f330b9..9f1bd918e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -6,8 +6,6 @@ import helmet from 'helmet'; import cors from 'cors'; import { DatabaseService } from './services'; import { setUpHealthEndpoint } from './services/health'; -import { TelemetryService } from './services'; - import cookieParser from 'cookie-parser'; import swaggerUi = require('swagger-ui-express'); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -72,10 +70,9 @@ import { getSmtpHost } from './config'; import { setup } from './utils/setup'; +import { patchRouterParam } from './utils/patchAsyncRoutes'; const main = async () => { - TelemetryService.logTelemetryMessage(); - await setup(); const app = express(); @@ -117,8 +114,8 @@ const main = async () => { app.use('/api/v1/membership', v1MembershipRouter); app.use('/api/v1/key', v1KeyRouter); app.use('/api/v1/invite-org', v1InviteOrgRouter); - app.use('/api/v1/secret', v1SecretRouter); - app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated + app.use('/api/v1/secret', v1SecretRouter); // deprecate + app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate app.use('/api/v1/password', v1PasswordRouter); app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/integration', v1IntegrationRouter); @@ -133,9 +130,9 @@ const main = async () => { app.use('/api/v2/workspace', v2EnvironmentRouter); app.use('/api/v2/workspace', v2TagsRouter); app.use('/api/v2/workspace', v2WorkspaceRouter); - app.use('/api/v2/secret', v2SecretRouter); // deprecated - app.use('/api/v2/secrets', v2SecretsRouter); - app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route + app.use('/api/v2/secret', v2SecretRouter); // deprecate + app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets + app.use('/api/v2/service-token', v2ServiceTokenDataRouter); app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new app.use('/api/v2/api-key', v2APIKeyDataRouter); @@ -146,7 +143,7 @@ const main = async () => { // api docs app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) - // Server status + // server status app.use('/api', healthCheck) //* Handle unrouted requests and respond with proper error message as well as status code diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 86ebf416c..fce9099eb 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -7,9 +7,6 @@ import { getAuthAPIKeyPayload, getAuthSAAKPayload } from '../helpers/auth'; -import { - UnauthorizedRequestError -} from '../utils/errors'; import { IUser, IServiceAccount, @@ -48,6 +45,7 @@ const requireAuth = ({ // validate auth token against accepted auth modes [acceptedAuthModes] // and return token type [authTokenType] and value [authTokenValue] + const { authMode, authTokenValue } = validateAuthMode({ headers: req.headers, acceptedAuthModes diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts index 089f570c8..2de8217da 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -1,9 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { Bot } from '../models'; -import { validateMembership } from '../helpers/membership'; -import { validateClientForBot } from '../helpers/bot'; -import { AccountNotFoundError } from '../utils/errors'; +import { validateClientForBot } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index bcde94f34..94d39a6c1 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -1,10 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { Integration, IntegrationAuth } from '../models'; -import { IntegrationService } from '../services'; -import { validateMembership } from '../helpers/membership'; -import { validateClientForIntegration } from '../helpers/integration'; -import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors'; +import { validateClientForIntegration } from '../validation'; /** * Validate if user on request is a member of workspace with proper roles associated diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index 8619fe084..2ffa7e230 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -1,10 +1,6 @@ import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; -import { IntegrationAuth, IWorkspace } from '../models'; -import { IntegrationService } from '../services'; -import { validateClientForIntegrationAuth } from '../helpers/integrationAuth'; -import { validateMembership } from '../helpers/membership'; -import { UnauthorizedRequestError } from '../utils/errors'; +import { validateClientForIntegrationAuth } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/middleware/requireMembershipAuth.ts b/backend/src/middleware/requireMembershipAuth.ts index 851230371..e03a6ea12 100644 --- a/backend/src/middleware/requireMembershipAuth.ts +++ b/backend/src/middleware/requireMembershipAuth.ts @@ -1,13 +1,6 @@ import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; -import { UnauthorizedRequestError } from '../utils/errors'; -import { - Membership, -} from '../models'; -import { - validateClientForMembership, - validateMembership -} from '../helpers/membership'; +import { validateClientForMembership } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/middleware/requireMembershipOrgAuth.ts b/backend/src/middleware/requireMembershipOrgAuth.ts index b34c9c5e2..dda90cae8 100644 --- a/backend/src/middleware/requireMembershipOrgAuth.ts +++ b/backend/src/middleware/requireMembershipOrgAuth.ts @@ -1,16 +1,6 @@ import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; -import { UnauthorizedRequestError } from '../utils/errors'; -import { - MembershipOrg -} from '../models'; -import { - validateClientForMembershipOrg, - validateMembershipOrg -} from '../helpers/membershipOrg'; - - -// TODO: transform +import { validateClientForMembershipOrg } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/middleware/requireOrganizationAuth.ts b/backend/src/middleware/requireOrganizationAuth.ts index f6d8eb8ce..5f7ef151d 100644 --- a/backend/src/middleware/requireOrganizationAuth.ts +++ b/backend/src/middleware/requireOrganizationAuth.ts @@ -1,9 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { IOrganization, MembershipOrg } from '../models'; -import { UnauthorizedRequestError, ValidationError } from '../utils/errors'; -import { validateMembershipOrg } from '../helpers/membershipOrg'; -import { validateClientForOrganization } from '../helpers/organization'; +import { validateClientForOrganization } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/middleware/requireSecretAuth.ts b/backend/src/middleware/requireSecretAuth.ts index 1462d67b0..4fda73a23 100644 --- a/backend/src/middleware/requireSecretAuth.ts +++ b/backend/src/middleware/requireSecretAuth.ts @@ -1,13 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; -import { Secret } from '../models'; -import { - validateMembership -} from '../helpers/membership'; -import { - validateClientForSecret -} from '../helpers/secrets'; +import { validateClientForSecret } from '../validation'; // note: used for old /v1/secret and /v2/secret routes. // newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception diff --git a/backend/src/middleware/requireSecretsAuth.ts b/backend/src/middleware/requireSecretsAuth.ts index a076a3f1a..f25487b97 100644 --- a/backend/src/middleware/requireSecretsAuth.ts +++ b/backend/src/middleware/requireSecretsAuth.ts @@ -1,8 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { UnauthorizedRequestError } from '../utils/errors'; -import { Secret, Membership } from '../models'; -import { validateClientForSecrets } from '../helpers/secrets'; +import { validateClientForSecrets } from '../validation'; const requireSecretsAuth = ({ acceptedRoles, diff --git a/backend/src/middleware/requireServiceAccountAuth.ts b/backend/src/middleware/requireServiceAccountAuth.ts index 40861a737..da690c7bb 100644 --- a/backend/src/middleware/requireServiceAccountAuth.ts +++ b/backend/src/middleware/requireServiceAccountAuth.ts @@ -1,15 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { ServiceAccount } from '../models'; -import { - ServiceAccountNotFoundError -} from '../utils/errors'; -import { - validateMembershipOrg -} from '../helpers/membershipOrg'; -import { - validateClientForServiceAccount -} from '../helpers/serviceAccount'; +import { validateClientForServiceAccount } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/middleware/requireServiceTokenDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts index 7715991ba..be93fd799 100644 --- a/backend/src/middleware/requireServiceTokenDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,9 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { ServiceToken, ServiceTokenData } from '../models'; -import { validateClientForServiceTokenData } from '../helpers/serviceTokenData'; -import { validateMembership } from '../helpers/membership'; -import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors'; +import { validateClientForServiceTokenData } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 76f723df2..557987490 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; import { validateMembership } from '../helpers/membership'; -import { validateClientForWorkspace } from '../helpers/workspace'; +import { validateClientForWorkspace } from '../validation'; import { UnauthorizedRequestError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts index 3dd90fb07..5755bfd8e 100644 --- a/backend/src/models/bot.ts +++ b/backend/src/models/bot.ts @@ -57,7 +57,8 @@ const botSchema = new Schema( algorithm: { // the encryption algorithm used type: String, enum: [ALGORITHM_AES_256_GCM], - required: true + required: true, + select: false }, keyEncoding: { type: String, @@ -65,7 +66,8 @@ const botSchema = new Schema( ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64 ], - required: true + required: true, + select: false } }, { diff --git a/backend/src/models/secretBlindIndexData.ts b/backend/src/models/secretBlindIndexData.ts index fc9896618..885faaff6 100644 --- a/backend/src/models/secretBlindIndexData.ts +++ b/backend/src/models/secretBlindIndexData.ts @@ -22,7 +22,7 @@ const secretBlindIndexDataSchema = new Schema( ref: 'Workspace', required: true }, - encryptedSaltCiphertext: { + encryptedSaltCiphertext: { // TODO: make these select: false type: String, required: true }, @@ -37,7 +37,8 @@ const secretBlindIndexDataSchema = new Schema( algorithm: { type: String, enum: [ALGORITHM_AES_256_GCM], - required: true + required: true, + select: false }, keyEncoding: { type: String, @@ -45,7 +46,8 @@ const secretBlindIndexDataSchema = new Schema( ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64 ], - required: true + required: true, + select: false } } diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index ab8e40488..0104505a9 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -7,9 +7,9 @@ import { requireSecretsAuth, validateRequest } from '../../middleware'; +import { validateClientForSecrets } from '../../validation'; import { query, body } from 'express-validator'; import { secretsController } from '../../controllers/v2'; -import { validateClientForSecrets } from '../../helpers/secrets'; import { ADMIN, MEMBER, diff --git a/backend/src/utils/crypto/index.ts b/backend/src/utils/crypto/index.ts index 0e7fb24b3..9c3877717 100644 --- a/backend/src/utils/crypto/index.ts +++ b/backend/src/utils/crypto/index.ts @@ -7,19 +7,14 @@ import { IEncryptAsymmetricOutput, IDecryptAsymmetricInput, IEncryptSymmetricInput, - IEncryptSymmetricOutput, IDecryptSymmetricInput } from '../../interfaces/utils'; -import { - BadRequestError, - InternalServerError -} from '../errors'; +import { BadRequestError } from '../errors'; import { ALGORITHM_AES_256_GCM, NONCE_BYTES_SIZE, BLOCK_SIZE_BYTES_16 } from '../../variables'; -import { validateEncryptionKey } from '../../validation'; /** * Return new base64, NaCl, public-private key pair. @@ -96,70 +91,6 @@ const decryptAsymmetric = ({ return util.encodeUTF8(plaintext); }; -/** - * Return symmetrically encrypted [plaintext] using [key]. - * @param {Object} obj - * @param {String} obj.plaintext - (utf8) plaintext to encrypt - * @param {String} obj.key - (base64) 256-bit key - * @returns {Object} obj - * @returns {String} obj.ciphertext (base64) ciphertext - * @returns {String} obj.iv (base64) iv - * @returns {String} obj.tag (base64) tag - */ -const encryptSymmetric = ({ - plaintext, - key -}: IEncryptSymmetricInput): IEncryptSymmetricOutput => { - validateEncryptionKey(key); - - const iv = crypto.randomBytes(NONCE_BYTES_SIZE); - const secretKey = crypto.createSecretKey(key, 'base64'); - const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, secretKey, iv); - - let ciphertext = cipher.update(plaintext, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -}; - -/** - * Return symmetrically decrypted [ciphertext] using [iv], [tag], - * and [key]. - * @param {Object} obj - * @param {String} obj.ciphertext - ciphertext to decrypt - * @param {String} obj.iv - (base64) 256-bit iv - * @param {String} obj.tag - (base64) tag - * @param {String} obj.key - (base64) 256-bit key - * @returns {String} cleartext - the deciphered ciphertext - */ -const decryptSymmetric = ({ - ciphertext, - iv, - tag, - key -}: IDecryptSymmetricInput): string => { - validateEncryptionKey(key); - - const secretKey = crypto.createSecretKey(key, 'base64'); - - const decipher = crypto.createDecipheriv( - ALGORITHM_AES_256_GCM, - secretKey, - Buffer.from(iv, 'base64') - ); - - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -}; - /** * Return symmetrically encrypted [plaintext] using [key]. * @@ -230,8 +161,6 @@ export { generateKeyPair, encryptAsymmetric, decryptAsymmetric, - encryptSymmetric, - decryptSymmetric, encryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8 }; diff --git a/backend/src/utils/setup/backfill.ts b/backend/src/utils/setup/backfill.ts deleted file mode 100644 index 8a964dfd7..000000000 --- a/backend/src/utils/setup/backfill.ts +++ /dev/null @@ -1,215 +0,0 @@ -import crypto from 'crypto'; -import { encryptSymmetric128BitHexKeyUTF8 } from '../crypto'; -import { EESecretService } from '../../ee/services'; -import { SecretVersion } from '../../ee/models'; -import { - Secret, - ISecret, - SecretBlindIndexData, - Workspace, - Bot, - BackupPrivateKey, - IntegrationAuth -} from '../../models'; -import { getEncryptionKey, getRootEncryptionKey } from '../../config'; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8 -} from '../../variables'; - -/** - * - */ -export const backfillSecretVersions = async () => { - await Secret.updateMany( - { version: { $exists: false } }, - { $set: { version: 1 } } - ); - - const unversionedSecrets: ISecret[] = await Secret.aggregate([ - { - $lookup: { - from: "secretversions", - localField: "_id", - foreignField: "secret", - as: "versions", - }, - }, - { - $match: { - versions: { $size: 0 }, - }, - }, - ]); - - if (unversionedSecrets.length > 0) { - await EESecretService.addSecretVersions({ - secretVersions: unversionedSecrets.map( - (s, idx) => - new SecretVersion({ - ...s, - secret: s._id, - version: s.version ? s.version : 1, - isDeleted: false, - workspace: s.workspace, - environment: s.environment, - }) - ), - }); - } -} - -export const backfillSecretBlindIndexData = async () => { - const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace'); - const workspaceIdsToBlindIndex = await Workspace.distinct('_id', { - _id: { - $nin: workspaceIdsBlindIndexed - } - }); - - const secretBlindIndexDataToInsert = await Promise.all( - workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => { - const salt = crypto.randomBytes(16).toString('base64'); - - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = encryptSymmetric128BitHexKeyUTF8({ - plaintext: salt, - key: await getEncryptionKey() - }); - - const secretBlindIndexData = new SecretBlindIndexData({ - workspace: workspaceToBlindIndex, - encryptedSaltCiphertext, - saltIV, - saltTag - }) - - return secretBlindIndexData; - }) - ); - - if (secretBlindIndexDataToInsert.length > 0) { - await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert); - } -} - -export const backfillEncryptionMetadata = async () => { - - // backfill bot encryption metadata - await Bot.updateMany( - { - algorithm: { - $exists: false - }, - keySize: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); - - // backfill secret blind index encryption metadata - await SecretBlindIndexData.updateMany( - { - algorithm: { - $exists: false - }, - keySize: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); - - // backfill backup private key encryption metadata - await BackupPrivateKey.updateMany( - { - algorithm: { - $exists: false - }, - keySize: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); - - // backfill integration auth encryption metadata - await IntegrationAuth.updateMany( - { - algorithm: { - $exists: false - }, - keySize: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); - - // TODO: blind indices - // TODO: secret versions and snapshots etc. - - // TODO: re-encrypt keys logic - // TODO: how do you handle different parts of the software - // encrypting under different schemes? - - // const encryptionKey = await getEncryptionKey(); - // const rootEncryptionKey = await getRootEncryptionKey(); - // console.log('rootEncryptionKey: ', rootEncryptionKey); - - // if (encryptionKey && rootEncryptionKey) { - // // case: both the old encryption key and new encryption key are present - // // -> perform migration if needed - // console.log('rootEncryptionKey is defined'); - - // const bots = await Bot.find({ - // algorithm: ALGORITHM_AES_256_GCM, - // keySize: 256, - // keyEncoding: ENCODING_SCHEME_UTF8 - // }, 'encryptedPrivateKey iv tag'); - - // if (bots.length > 0) { - // // TODO: unencrypt and re-encrypt - // // TODO: unencrypt and re-encrypt blind-indices - // // probably then need to move this function - - // console.log('bots: ', bots); - // } - // } -} diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts new file mode 100644 index 000000000..cffb16476 --- /dev/null +++ b/backend/src/utils/setup/backfillData.ts @@ -0,0 +1,324 @@ +import crypto from 'crypto'; +import { encryptSymmetric128BitHexKeyUTF8 } from '../crypto'; +import { EESecretService } from '../../ee/services'; +import { SecretVersion } from '../../ee/models'; +import { + Secret, + ISecret, + SecretBlindIndexData, + Workspace, + Bot, + BackupPrivateKey, + IntegrationAuth, +} from '../../models'; +import { + generateKeyPair +} from '../../utils/crypto'; +import { + client, + getEncryptionKey, + getRootEncryptionKey +} from '../../config'; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 +} from '../../variables'; +import { InternalServerError } from '../errors'; + +/** + * Backfill secrets to ensure that they're all versioned and have + * corresponding secret versions + */ +export const backfillSecretVersions = async () => { + await Secret.updateMany( + { version: { $exists: false } }, + { $set: { version: 1 } } + ); + + const unversionedSecrets: ISecret[] = await Secret.aggregate([ + { + $lookup: { + from: "secretversions", + localField: "_id", + foreignField: "secret", + as: "versions", + }, + }, + { + $match: { + versions: { $size: 0 }, + }, + }, + ]); + + if (unversionedSecrets.length > 0) { + await EESecretService.addSecretVersions({ + secretVersions: unversionedSecrets.map( + (s, idx) => + new SecretVersion({ + ...s, + secret: s._id, + version: s.version ? s.version : 1, + isDeleted: false, + workspace: s.workspace, + environment: s.environment, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }) + ), + }); + } +} + +/** + * Backfill workspace bots to ensure that every workspace has a bot + */ +export const backfillBots = async () => { + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + const workspaceIdsWithBot = await Bot.distinct('workspace'); + const workspaceIdsToAddBot = await Workspace.distinct('_id', { + _id: { + $nin: workspaceIdsWithBot + } + }); + + if (workspaceIdsToAddBot.length === 0) return; + + const botsToInsert = await Promise.all( + workspaceIdsToAddBot.map(async (workspaceToAddBot) => { + const { publicKey, privateKey } = generateKeyPair(); + + if (rootEncryptionKey) { + const { + ciphertext: encryptedPrivateKey, + iv, + tag + } = client.encryptSymmetric(privateKey, rootEncryptionKey); + + return new Bot({ + name: 'Infisical Bot', + workspace: workspaceToAddBot, + isActive: false, + publicKey, + encryptedPrivateKey, + iv, + tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64 + }); + } else if (encryptionKey) { + const { + ciphertext: encryptedPrivateKey, + iv, + tag + } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: privateKey, + key: encryptionKey + }); + + return new Bot({ + name: 'Infisical Bot', + workspace: workspaceToAddBot, + isActive: false, + publicKey, + encryptedPrivateKey, + iv, + tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }); + } + + throw InternalServerError({ + message: 'Failed to backfill workspace bots due to missing encryption key' + }); + }) + ); + + await Bot.insertMany(botsToInsert); +} + +/** + * Backfill secret blind index data to ensure that every workspace + * has a secret blind index data + */ +export const backfillSecretBlindIndexData = async () => { + + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace'); + const workspaceIdsToBlindIndex = await Workspace.distinct('_id', { + _id: { + $nin: workspaceIdsBlindIndexed + } + }); + + if (workspaceIdsToBlindIndex.length === 0) return; + + const secretBlindIndexDataToInsert = await Promise.all( + workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => { + const salt = crypto.randomBytes(16).toString('base64'); + + if (rootEncryptionKey) { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag + } = client.encryptSymmetric(salt, rootEncryptionKey) + + return new SecretBlindIndexData({ + workspace: workspaceToBlindIndex, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64 + }); + } else if (encryptionKey) { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag + } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: salt, + key: encryptionKey + }); + + return new SecretBlindIndexData({ + workspace: workspaceToBlindIndex, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }); + } + + throw InternalServerError({ + message: 'Failed to backfill secret blind index data due to missing encryption key' + }); + }) + ); + + SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert); +} + +/** + * Backfill Secret, SecretVersion, SecretBlindIndexData, Bot, + * BackupPrivateKey, IntegrationAuth collections to ensure that + * they all have encryption metadata documented + */ +export const backfillEncryptionMetadata = async () => { + + // backfill secret encryption metadata + await Secret.updateMany( + { + algorithm: { + $exists: false + }, + keyEncoding: { + $exists: false + } + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + ); + + // backfill secret version encryption metadata + await SecretVersion.updateMany( + { + algorithm: { + $exists: false + }, + keyEncoding: { + $exists: false + } + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + ); + + // backfill secret blind index encryption metadata + await SecretBlindIndexData.updateMany( + { + algorithm: { + $exists: false + }, + keyEncoding: { + $exists: false + } + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + ); + + // backfill bot encryption metadata + await Bot.updateMany( + { + algorithm: { + $exists: false + }, + keyEncoding: { + $exists: false + } + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + ); + + // backfill backup private key encryption metadata + await BackupPrivateKey.updateMany( + { + algorithm: { + $exists: false + }, + keyEncoding: { + $exists: false + } + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + ); + + // backfill integration auth encryption metadata + await IntegrationAuth.updateMany( + { + algorithm: { + $exists: false + }, + keyEncoding: { + $exists: false + } + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + ); +} \ No newline at end of file diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 059edf6f6..6c7bfb422 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -1,15 +1,23 @@ import * as Sentry from '@sentry/node'; -import { DatabaseService } from '../../services'; +import { DatabaseService, TelemetryService } from '../../services'; import { setTransporter } from '../../helpers/nodemailer'; import { initSmtp } from '../../services/smtp'; import { createTestUserForDevelopment } from '../addDevelopmentUser' // eslint-disable-next-line @typescript-eslint/no-var-requires const { patchRouterParam } = require('../patchAsyncRoutes'); +import { + validateEncryptionKeysConfig +} from './validateConfig'; import { backfillSecretVersions, + backfillBots, backfillSecretBlindIndexData, backfillEncryptionMetadata -} from './backfill'; +} from './backfillData'; +import { + reencryptBotPrivateKeys, + reencryptSecretBlindIndexDataSalts +} from './reencryptData'; import { getNodeEnv, getMongoURL, @@ -18,34 +26,48 @@ import { /** * Prepare Infisical upon startup. This includes tasks like: + * - Log initial telemetry message * - Initializing SMTP configuration * - Initializing the database connection * - Initializing Sentry * - Backfilling data + * - Re-encrypting data */ export const setup = async () => { + patchRouterParam(); + await validateEncryptionKeysConfig(); + await TelemetryService.logTelemetryMessage(); + // initializing SMTP configuration setTransporter(await initSmtp()); // initializing the database connection await DatabaseService.initDatabase(await getMongoURL()); + + /** + * NOTE: the order in this setup function is critical. + * It is important to backfill data before performing any re-encryption functionality. + */ - // backfilling data + // backfilling data to catch up with new collections and updated fields await backfillSecretVersions(); + await backfillBots(); await backfillSecretBlindIndexData(); await backfillEncryptionMetadata(); - // initializing Sentry - if ((await getNodeEnv()) !== 'development') { - Sentry.init({ - dsn: await getSentryDSN(), - tracesSampleRate: 1.0, - debug: await getNodeEnv() === 'production' ? false : true, - environment: await getNodeEnv() - }); - } + // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY + // to base64 256-bit ROOT_ENCRYPTION_KEY + await reencryptBotPrivateKeys(); + await reencryptSecretBlindIndexDataSalts(); + + // initializing Sentry + Sentry.init({ + dsn: await getSentryDSN(), + tracesSampleRate: 1.0, + debug: (await getNodeEnv()) === 'production' ? false : true, + environment: (await getNodeEnv()) + }); - patchRouterParam(); await createTestUserForDevelopment(); } diff --git a/backend/src/utils/setup/reencryptData.ts b/backend/src/utils/setup/reencryptData.ts new file mode 100644 index 000000000..1bf499cc9 --- /dev/null +++ b/backend/src/utils/setup/reencryptData.ts @@ -0,0 +1,126 @@ +import { + Bot, + IBot, + ISecretBlindIndexData, + SecretBlindIndexData +} from '../../models'; +import { decryptSymmetric128BitHexKeyUTF8 } from '../../utils/crypto'; +import { + client, + getEncryptionKey, + getRootEncryptionKey +} from '../../config'; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 +} from '../../variables'; + +/** + * Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY + * to base64 256-bit ROOT_ENCRYPTION_KEY + */ +export const reencryptBotPrivateKeys = async () => { + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + if (encryptionKey && rootEncryptionKey) { + // 1: re-encrypt bot private keys under ROOT_ENCRYPTION_KEY + const bots = await Bot.find({ + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }).select('+encryptedPrivateKey iv tag algorithm keyEncoding'); + + if (bots.length === 0) return; + + const operationsBot = await Promise.all( + bots.map(async (bot: IBot) => { + + const privateKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: bot.encryptedPrivateKey, + iv: bot.iv, + tag: bot.tag, + key: encryptionKey + }); + + const { + ciphertext: encryptedPrivateKey, + iv, + tag + } = client.encryptSymmetric(privateKey, rootEncryptionKey); + + return ({ + updateOne: { + filter: { + _id: bot._id + }, + update: { + encryptedPrivateKey, + iv, + tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64 + } + } + }) + }) + ); + + await Bot.bulkWrite(operationsBot); + } +} + +/** + * Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY + * to base64 256-bit ROOT_ENCRYPTION_KEY + */ +export const reencryptSecretBlindIndexDataSalts = async () => { + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + // 2. re-encrypt secret blind index salts under ROOT_ENCRYPTION_KEY + + if (encryptionKey && rootEncryptionKey) { + const secretBlindIndexData = await SecretBlindIndexData.find({ + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }).select('+encryptedSaltCiphertext +saltIV +saltTag +algorithm +keyEncoding'); + + if (secretBlindIndexData.length == 0) return; + + const operationsSecretBlindIndexData = await Promise.all( + secretBlindIndexData.map(async (secretBlindIndexDatum: ISecretBlindIndexData) => { + + const salt = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secretBlindIndexDatum.encryptedSaltCiphertext, + iv: secretBlindIndexDatum.saltIV, + tag: secretBlindIndexDatum.saltTag, + key: encryptionKey + }); + + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag + } = client.encryptSymmetric(salt, rootEncryptionKey); + + return ({ + updateOne: { + filter: { + _id: secretBlindIndexDatum._id + }, + update: { + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64 + } + } + }) + }) + ); + + await SecretBlindIndexData.bulkWrite(operationsSecretBlindIndexData); + } +} \ No newline at end of file diff --git a/backend/src/utils/setup/validateConfig.ts b/backend/src/utils/setup/validateConfig.ts new file mode 100644 index 000000000..b71e6e4d9 --- /dev/null +++ b/backend/src/utils/setup/validateConfig.ts @@ -0,0 +1,69 @@ +import { + getEncryptionKey, + getRootEncryptionKey +} from '../../config'; +import { + InternalServerError +} from '../../utils/errors'; + +/** + * Validate ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY. Specifically: + * - ENCRYPTION_KEY is a hex, 128-bit string + * - ROOT_ENCRYPTION_KEY is a base64, 128-bit string + * - Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY are present + * + * - Encrypted data is consistent with the passed in encryption keys + * + * NOTE 1: ENCRYPTION_KEY is being transitioned to ROOT_ENCRYPTION_KEY + * NOTE 2: In the future, we will have a superior validation function + * built into the SDK. + */ +export const validateEncryptionKeysConfig = async () => { + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + // TODO: handle case where either of keys can be empty strings + // and it would actually count as being defined for encryption + // within the application + + // console.log('validateEncryptionKeysConfig'); + // console.log('encryptionKey: ', encryptionKey); + // console.log('rootEncryptionKey: ', rootEncryptionKey); + + if ( + (encryptionKey === undefined || encryptionKey === "") && + (rootEncryptionKey === undefined || rootEncryptionKey === "") + ) throw InternalServerError({ + message: "Failed to find required root encryption key environment variable. Please make sure that you're passing in a ROOT_ENCRYPTION_KEY environment variable." + }); + + if (encryptionKey && encryptionKey !== '') { + // validate [encryptionKey] + + const keyBuffer = Buffer.from(encryptionKey, 'hex'); + const decoded = keyBuffer.toString('hex'); + + if (decoded !== encryptionKey) throw InternalServerError({ + message: 'Failed to validate that the encryption key is correctly encoded in hex.' + }); + + if (keyBuffer.length !== 16) throw InternalServerError({ + message: 'Failed to validate that the encryption key is a 128-bit hex string.' + }); + } + + if (rootEncryptionKey && rootEncryptionKey !== '') { + // validate [rootEncryptionKey] + + const keyBuffer = Buffer.from(rootEncryptionKey, 'base64') + const decoded = keyBuffer.toString('base64'); + + if (decoded !== rootEncryptionKey) throw InternalServerError({ + message: 'Failed to validate that the root encryption key is correctly encoded in base64' + }); + + if (keyBuffer.length !== 32) throw InternalServerError({ + message: 'Failed to validate that the encryption key is a 256-bit base64 string' + }); + } +} \ No newline at end of file diff --git a/backend/src/validation/bot.ts b/backend/src/validation/bot.ts new file mode 100644 index 000000000..7104eec33 --- /dev/null +++ b/backend/src/validation/bot.ts @@ -0,0 +1,98 @@ +import { Types } from 'mongoose'; +import { + IUser, + IServiceAccount, + IServiceTokenData, + Bot, + User, + ServiceAccount, + ServiceTokenData +} from '../models'; +import { validateServiceAccountClientForWorkspace } from './serviceAccount'; +import { validateUserClientForWorkspace } from './user'; +import { + UnauthorizedRequestError, + BotNotFoundError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for bot with id [botId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.botId - id of bot to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + */ +export const validateClientForBot = async ({ + authData, + botId, + acceptedRoles, +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + botId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; +}) => { + const bot = await Bot.findById(botId); + + if (!bot) throw BotNotFoundError(); + + if ( + authData.authMode === AUTH_MODE_JWT && + authData.authPayload instanceof User + ) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: bot.workspace, + acceptedRoles, + }); + + return bot; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && + authData.authPayload instanceof ServiceAccount + ) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: bot.workspace, + }); + + return bot; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_TOKEN && + authData.authPayload instanceof ServiceTokenData + ) { + throw UnauthorizedRequestError({ + message: "Failed service token authorization for bot", + }); + } + + if ( + authData.authMode === AUTH_MODE_API_KEY && + authData.authPayload instanceof User + ) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: bot.workspace, + acceptedRoles, + }); + + return bot; + } + + throw BotNotFoundError({ + message: "Failed client authorization for bot", + }); +}; \ No newline at end of file diff --git a/backend/src/validation/config.ts b/backend/src/validation/config.ts deleted file mode 100644 index c96164929..000000000 --- a/backend/src/validation/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { InternalServerError } from "../utils/errors"; - -/** - * Validate that the encryption key [encryptionKey] is in base64 format and 256-bit - * @param {String} encryptionKey - the encryption key to validate - */ -export const validateEncryptionKey = (encryptionKey: string): Buffer => { - - const keyBuffer = Buffer.from(encryptionKey, 'base64') - const decoded = keyBuffer.toString('base64'); - - if (decoded !== encryptionKey) throw InternalServerError({ - message: 'Failed to validate the format of the encryption key. Please check that it is in base64 format.' - }); - - if (keyBuffer.length !== 32) throw InternalServerError({ - message: 'Failed to validate that the encryption key is 256-bit. Please check that it is 256-bit.' - }); - - return keyBuffer; -}; \ No newline at end of file diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index de8bde7ba..84f25bb74 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -1 +1,10 @@ -export * from './config'; \ No newline at end of file +export * from './workspace'; +export * from './bot'; +export * from './integration'; +export * from './integrationAuth'; +export * from './membership'; +export * from './membershipOrg'; +export * from './organization'; +export * from './secrets'; +export * from './serviceAccount'; +export * from './serviceTokenData'; \ No newline at end of file diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts new file mode 100644 index 000000000..5b2f4ad3c --- /dev/null +++ b/backend/src/validation/integration.ts @@ -0,0 +1,103 @@ +import { Types } from 'mongoose'; +import { + IUser, + IServiceAccount, + IServiceTokenData, + Integration, + IntegrationAuth, + User, + ServiceAccount, + ServiceTokenData +} from '../models'; +import { validateServiceAccountClientForWorkspace } from './serviceAccount'; +import { validateUserClientForWorkspace } from './user'; +import { IntegrationService } from '../services'; +import { + IntegrationNotFoundError, + IntegrationAuthNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for integration with id [integrationId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.integrationId - id of integration to validate against + * @param {String} obj.environment - (optional) environment in workspace to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +export const validateClientForIntegration = async ({ + authData, + integrationId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + integrationId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + + const integration = await Integration.findById(integrationId); + if (!integration) throw IntegrationNotFoundError(); + + const integrationAuth = await IntegrationAuth + .findById(integration.integrationAuth) + .select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); + + if (!integrationAuth) throw IntegrationAuthNotFoundError(); + + const accessToken = (await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + })).accessToken; + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integration.workspace, + acceptedRoles + }); + + return ({ integration, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: integration.workspace + }); + + return ({ integration, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for integration' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integration.workspace, + acceptedRoles + }); + + return ({ integration, accessToken }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for integration' + }); +} \ No newline at end of file diff --git a/backend/src/helpers/integrationAuth.ts b/backend/src/validation/integrationAuth.ts similarity index 95% rename from backend/src/helpers/integrationAuth.ts rename to backend/src/validation/integrationAuth.ts index c169fb799..b43dd4cbd 100644 --- a/backend/src/helpers/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -20,8 +20,8 @@ import { UnauthorizedRequestError } from '../utils/errors'; import { IntegrationService } from '../services'; -import { validateUserClientForWorkspace } from '../helpers/user'; -import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; +import { validateUserClientForWorkspace } from './user'; +import { validateServiceAccountClientForWorkspace } from './serviceAccount'; /** * Validate authenticated clients for integration authorization with id [integrationAuthId] based diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts new file mode 100644 index 000000000..ab4f8dc76 --- /dev/null +++ b/backend/src/validation/membership.ts @@ -0,0 +1,94 @@ +import { Types } from 'mongoose'; +import { + IUser, + IServiceAccount, + IServiceTokenData, + Membership, + User, + ServiceAccount, + ServiceTokenData +} from '../models'; +import { validateServiceAccountClientForWorkspace } from './serviceAccount'; +import { validateUserClientForWorkspace } from './user'; +import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData'; +import { + MembershipNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for membership with id [membershipId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.membershipId - id of membership to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles + * @returns {Membership} - validated membership + */ +export const validateClientForMembership = async ({ + authData, + membershipId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + membershipId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + + const membership = await Membership.findById(membershipId); + + if (!membership) throw MembershipNotFoundError({ + message: 'Failed to find membership' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: membership.workspace, + acceptedRoles + }); + + return membership; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: membership.workspace + }); + + return membership; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId: new Types.ObjectId(membership.workspace) + }); + + return membership; + } + + if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: membership.workspace, + acceptedRoles + }); + + return membership; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for membership' + }); +} \ No newline at end of file diff --git a/backend/src/validation/membershipOrg.ts b/backend/src/validation/membershipOrg.ts new file mode 100644 index 000000000..7fd86a374 --- /dev/null +++ b/backend/src/validation/membershipOrg.ts @@ -0,0 +1,93 @@ +import { Types } from 'mongoose'; +import { + IUser, + IServiceAccount, + IServiceTokenData, + MembershipOrg, + User, + ServiceAccount, + ServiceTokenData +} from '../models'; +import { + validateMembershipOrg +} from '../helpers/membershipOrg'; +import { + MembershipOrgNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for organization membership with id [membershipOrgId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against + * @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles + * @param {MembershipOrg} - validated organization membership + */ +export const validateClientForMembershipOrg = async ({ + authData, + membershipOrgId, + acceptedRoles, + acceptedStatuses +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + membershipOrgId: Types.ObjectId; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; +}) => { + const membershipOrg = await MembershipOrg.findById(membershipOrgId); + + if (!membershipOrg) throw MembershipOrgNotFoundError({ + message: 'Failed to find organization membership ' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateMembershipOrg({ + userId: authData.authPayload._id, + organizationId: membershipOrg.organization, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({ + message: 'Failed service account client authorization for organization membership' + }); + + return membershipOrg; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service account client authorization for organization membership' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateMembershipOrg({ + userId: authData.authPayload._id, + organizationId: membershipOrg.organization, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for organization membership' + }); +} \ No newline at end of file diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts new file mode 100644 index 000000000..239517b75 --- /dev/null +++ b/backend/src/validation/organization.ts @@ -0,0 +1,104 @@ +import { Types } from 'mongoose'; +import { + IUser, + IServiceAccount, + IServiceTokenData, + Organization, + User, + ServiceAccount, + ServiceTokenData +} from '../models'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; +import { + OrganizationNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; +import { validateUserClientForOrganization } from './user'; +import { validateServiceAccountClientForOrganization } from './serviceAccount'; + +/** + * Validate accepted clients for organization with id [organizationId] + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.organizationId - id of organization to validate against + */ +export const validateClientForOrganization = async ({ + authData, + organizationId, + acceptedRoles, + acceptedStatuses, +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + organizationId: Types.ObjectId; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; +}) => { + const organization = await Organization.findById(organizationId); + + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization", + }); + } + + if ( + authData.authMode === AUTH_MODE_JWT && + authData.authPayload instanceof User + ) { + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses, + }); + + return { organization, membershipOrg }; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && + authData.authPayload instanceof ServiceAccount + ) { + await validateServiceAccountClientForOrganization({ + serviceAccount: authData.authPayload, + organization, + }); + + return { organization }; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_TOKEN && + authData.authPayload instanceof ServiceTokenData + ) { + throw UnauthorizedRequestError({ + message: "Failed service token authorization for organization", + }); + } + + if ( + authData.authMode === AUTH_MODE_API_KEY && + authData.authPayload instanceof User + ) { + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses, + }); + + return { organization, membershipOrg }; + } + + throw UnauthorizedRequestError({ + message: "Failed client authorization for organization", + }); +}; \ No newline at end of file diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts new file mode 100644 index 000000000..272fe4545 --- /dev/null +++ b/backend/src/validation/secrets.ts @@ -0,0 +1,174 @@ +import { Types } from 'mongoose'; +import { + ISecret, + Secret, + User, + ServiceAccount, + ServiceTokenData +} from '../models'; +import { validateServiceAccountClientForWorkspace, validateServiceAccountClientForSecrets } from './serviceAccount'; +import { validateUserClientForSecret, validateUserClientForSecrets } from './user'; +import { validateServiceTokenDataClientForWorkspace, validateServiceTokenDataClientForSecrets } from './serviceTokenData'; +import { AuthData } from '../interfaces/middleware'; +import { + SecretNotFoundError, + UnauthorizedRequestError, + BadRequestError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for secrets with id [secretId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.secretId - id of secret to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +export const validateClientForSecret = async ({ + authData, + secretId, + acceptedRoles, + requiredPermissions +}: { + authData: AuthData; + secretId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions: string[]; +}) => { + const secret = await Secret.findById(secretId); + + if (!secret) throw SecretNotFoundError({ + message: 'Failed to find secret' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for secret' + }); +} + +/** + * Validate authenticated clients for secrets with ids [secretIds] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against + * @param {String} obj.environment - (optional) environment in workspace to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +export const validateClientForSecrets = async ({ + authData, + secretIds, + requiredPermissions +}: { + authData: AuthData; + secretIds: Types.ObjectId[]; + requiredPermissions: string[]; +}) => { + + let secrets: ISecret[] = []; + + secrets = await Secret.find({ + _id: { + $in: secretIds + } + }); + + if (secrets.length != secretIds.length) { + throw BadRequestError({ message: 'Failed to validate non-existent secrets' }) + } + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForSecrets({ + user: authData.authPayload, + secrets, + requiredPermissions + }); + + return secrets; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForSecrets({ + serviceAccount: authData.authPayload, + secrets, + requiredPermissions + }); + + return secrets; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForSecrets({ + serviceTokenData: authData.authPayload, + secrets, + requiredPermissions + }); + + return secrets; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForSecrets({ + user: authData.authPayload, + secrets, + requiredPermissions + }); + + return secrets; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for secrets resource' + }); +} \ No newline at end of file diff --git a/backend/src/helpers/serviceAccount.ts b/backend/src/validation/serviceAccount.ts similarity index 92% rename from backend/src/helpers/serviceAccount.ts rename to backend/src/validation/serviceAccount.ts index 892767259..11997763c 100644 --- a/backend/src/helpers/serviceAccount.ts +++ b/backend/src/validation/serviceAccount.ts @@ -9,9 +9,9 @@ import { IServiceTokenData, ISecret, IOrganization, - IServiceAccountWorkspacePermission, ServiceAccountWorkspacePermission } from '../models'; +import { validateUserClientForServiceAccount } from './user'; import { BadRequestError, UnauthorizedRequestError, @@ -25,11 +25,8 @@ import { AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY } from '../variables'; -import { - validateUserClientForServiceAccount -} from '../helpers/user'; -const validateClientForServiceAccount = async ({ +export const validateClientForServiceAccount = async ({ authData, serviceAccountId, requiredPermissions @@ -100,7 +97,7 @@ const validateClientForServiceAccount = async ({ * @param {String} environment - (optional) environment in workspace to validate against * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ - const validateServiceAccountClientForWorkspace = async ({ +export const validateServiceAccountClientForWorkspace = async ({ serviceAccount, workspaceId, environment, @@ -169,7 +166,7 @@ const validateClientForServiceAccount = async ({ * @param {Secret[]} secrets - secrets to validate against * @param {string[]} requiredPermissions - required permissions as part of the endpoint */ - const validateServiceAccountClientForSecrets = async ({ +export const validateServiceAccountClientForSecrets = async ({ serviceAccount, secrets, requiredPermissions @@ -226,7 +223,7 @@ const validateClientForServiceAccount = async ({ * @param {ServiceAccount} targetServiceAccount - target service account to validate against * @param {string[]} requiredPermissions - required permissions as part of the endpoint */ -const validateServiceAccountClientForServiceAccount = ({ +export const validateServiceAccountClientForServiceAccount = ({ serviceAccount, targetServiceAccount, requiredPermissions @@ -248,7 +245,7 @@ const validateServiceAccountClientForServiceAccount = ({ * @param {User} obj.user - service account client * @param {Organization} obj.organization - organization to validate against */ -const validateServiceAccountClientForOrganization = async ({ +export const validateServiceAccountClientForOrganization = async ({ serviceAccount, organization }: { @@ -260,12 +257,4 @@ const validateServiceAccountClientForOrganization = async ({ message: 'Failed service account authorization for the given organization' }); } -} - -export { - validateClientForServiceAccount, - validateServiceAccountClientForWorkspace, - validateServiceAccountClientForSecrets, - validateServiceAccountClientForServiceAccount, - validateServiceAccountClientForOrganization } \ No newline at end of file diff --git a/backend/src/helpers/serviceTokenData.ts b/backend/src/validation/serviceTokenData.ts similarity index 92% rename from backend/src/helpers/serviceTokenData.ts rename to backend/src/validation/serviceTokenData.ts index ecf46a4d1..8713e000f 100644 --- a/backend/src/helpers/serviceTokenData.ts +++ b/backend/src/validation/serviceTokenData.ts @@ -18,8 +18,8 @@ import { AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY } from '../variables'; -import { validateUserClientForWorkspace } from '../helpers/user'; -import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; +import { validateUserClientForWorkspace } from './user'; +import { validateServiceAccountClientForWorkspace } from './serviceAccount'; /** * Validate authenticated clients for service token with id [serviceTokenId] based @@ -29,7 +29,7 @@ import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAcco * @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles */ -const validateClientForServiceTokenData = async ({ +export const validateClientForServiceTokenData = async ({ authData, serviceTokenDataId, acceptedRoles @@ -100,7 +100,7 @@ const validateClientForServiceTokenData = async ({ * @param {String} environment - (optional) environment in workspace to validate against * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ - const validateServiceTokenDataClientForWorkspace = async ({ +export const validateServiceTokenDataClientForWorkspace = async ({ serviceTokenData, workspaceId, environment, @@ -146,7 +146,7 @@ const validateClientForServiceTokenData = async ({ * @param {Secret[]} secrets - secrets to validate against * @param {string[]} requiredPermissions - required permissions as part of the endpoint */ - const validateServiceTokenDataClientForSecrets = async ({ +export const validateServiceTokenDataClientForSecrets = async ({ serviceTokenData, secrets, requiredPermissions @@ -179,10 +179,4 @@ const validateClientForServiceTokenData = async ({ } }); }); -} - -export { - validateClientForServiceTokenData, - validateServiceTokenDataClientForWorkspace, - validateServiceTokenDataClientForSecrets } \ No newline at end of file diff --git a/backend/src/validation/user.ts b/backend/src/validation/user.ts new file mode 100644 index 000000000..1329adfc4 --- /dev/null +++ b/backend/src/validation/user.ts @@ -0,0 +1,209 @@ +import { Types } from 'mongoose'; +import { + IUser, + ISecret, + IServiceAccount, + Membership, + IOrganization, +} from '../models'; +import { validateMembership } from '../helpers/membership'; +import _ from 'lodash'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { + validateMembershipOrg +} from '../helpers/membershipOrg'; +import { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from '../variables'; + +/** + * Validate that user (client) can access workspace + * with id [workspaceId] and its environment [environment] with required permissions + * [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against + * @param {String} environment - (optional) environment in workspace to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +export const validateUserClientForWorkspace = async ({ + user, + workspaceId, + environment, + acceptedRoles, + requiredPermissions +}: { + user: IUser; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions?: string[]; +}) => { + + // validate user membership in workspace + const membership = await validateMembership({ + userId: user._id, + workspaceId, + acceptedRoles + }); + + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); + break; + case PERMISSION_WRITE_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + + return membership; +} + +/** + * Validate that user (client) can access secret [secret] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Secret[]} obj.secrets - secrets to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +export const validateUserClientForSecret = async ({ + user, + secret, + acceptedRoles, + requiredPermissions +}: { + user: IUser; + secret: ISecret; + acceptedRoles?: Array<'admin' | 'member'>; + requiredPermissions?: string[]; +}) => { + const membership = await validateMembership({ + userId: user._id, + workspaceId: secret.workspace, + acceptedRoles + }); + + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: 'You do not have the required permissions to perform this action' + }); + } + } +} + +/** + * Validate that user (client) can access secrets [secrets] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Secret[]} obj.secrets - secrets to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +export const validateUserClientForSecrets = async ({ + user, + secrets, + requiredPermissions +}: { + user: IUser; + secrets: ISecret[]; + requiredPermissions?: string[]; +}) => { + + // TODO: add acceptedRoles? + + const userMemberships = await Membership.find({ user: user._id }) + const userMembershipById = _.keyBy(userMemberships, 'workspace'); + const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); + + // for each secret check if the secret belongs to a workspace the user is a member of + secrets.forEach((secret: ISecret) => { + if (!workspaceIdsSet.has(secret.workspace.toString())) { + throw BadRequestError({ + message: 'Failed authorization for the secret' + }); + } + + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; + const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: 'You do not have the required permissions to perform this action' + }); + } + } + }); +} + +/** + * Validate that user (client) can access service account [serviceAccount] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {ServiceAccount} obj.serviceAccount - service account to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +export const validateUserClientForServiceAccount = async ({ + user, + serviceAccount, + requiredPermissions +}: { + user: IUser; + serviceAccount: IServiceAccount; + requiredPermissions?: string[]; +}) => { + if (!serviceAccount.user.equals(user._id)) { + // case: user who created service account is not the + // same user that is on the request + await validateMembershipOrg({ + userId: user._id, + organizationId: serviceAccount.organization, + acceptedRoles: [], + acceptedStatuses: [] + }); + } +} + +/** + * Validate that user (client) can access organization [organization] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Organization} obj.organization - organization to validate against + */ +export const validateUserClientForOrganization = async ({ + user, + organization, + acceptedRoles, + acceptedStatuses +}: { + user: IUser; + organization: IOrganization; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; +}) => { + const membershipOrg = await validateMembershipOrg({ + userId: user._id, + organizationId: organization._id, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; +} \ No newline at end of file diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts new file mode 100644 index 000000000..60d13b129 --- /dev/null +++ b/backend/src/validation/workspace.ts @@ -0,0 +1,124 @@ +import { Types } from 'mongoose'; +import { + IUser, + IServiceAccount, + IServiceTokenData, + Workspace, + User, + ServiceAccount, + ServiceTokenData, + SecretBlindIndexData +} from '../models'; +import { validateServiceAccountClientForWorkspace } from './serviceAccount'; +import { validateUserClientForWorkspace } from './user'; +import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData'; +import { + UnauthorizedRequestError, + WorkspaceNotFoundError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for workspace with id [workspaceId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against + * @param {String} obj.environment - (optional) environment in workspace to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +export const validateClientForWorkspace = async ({ + authData, + workspaceId, + environment, + acceptedRoles, + requiredPermissions, + requireBlindIndicesEnabled +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions?: string[]; + requireBlindIndicesEnabled: boolean; +}) => { + + const workspace = await Workspace.findById(workspaceId); + + if (!workspace) throw WorkspaceNotFoundError({ + message: 'Failed to find workspace' + }); + + 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) + + 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' + }); + } + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + const membership = await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId, + environment, + acceptedRoles, + requiredPermissions + }); + + return ({ membership }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId, + environment, + requiredPermissions + }); + + return {}; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId, + environment, + requiredPermissions + }); + + return {}; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + const membership = await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId, + environment, + acceptedRoles, + requiredPermissions + }); + + return ({ membership }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for workspace' + }); +} diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index 1f1099c7b..97ea07e7c 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -32,7 +32,6 @@ export default function SignUp() { const router = useRouter(); const {data: serverDetails } = useFetchServerStatus() - const { t } = useTranslation(); useEffect(() => {