From 099cee7f3926b73a3f5467bcf17e0e512abbc126 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 3 May 2023 14:21:42 +0300 Subject: [PATCH 01/22] Begin refactoring backfilling and preparation operations into setup and start adding encryption metadata to models --- backend/src/config/index.ts | 14 +- backend/src/ee/helpers/secret.ts | 46 +--- backend/src/ee/services/EESecretService.ts | 12 +- backend/src/helpers/bot.ts | 23 +- backend/src/helpers/database.ts | 6 +- backend/src/helpers/secrets.ts | 55 +---- backend/src/helpers/workspace.ts | 1 - backend/src/index.ts | 21 +- backend/src/interfaces/utils/crypto.ts | 41 ++++ backend/src/interfaces/utils/index.ts | 1 + backend/src/models/backupPrivateKey.ts | 26 +++ backend/src/models/bot.ts | 27 +++ backend/src/models/integrationAuth.ts | 24 +++ backend/src/models/secretBlindIndexData.ts | 27 +++ backend/src/services/SecretService.ts | 12 -- backend/src/utils/crypto.ts | 139 ------------ backend/src/utils/crypto/index.ts | 237 +++++++++++++++++++++ backend/src/utils/setup/backfill.ts | 210 ++++++++++++++++++ backend/src/utils/setup/index.ts | 51 +++++ backend/src/validation/config.ts | 21 ++ backend/src/validation/index.ts | 1 + backend/src/variables/action.ts | 21 +- backend/src/variables/authentication.ts | 15 +- backend/src/variables/crypto.ts | 7 + backend/src/variables/environment.ts | 18 +- backend/src/variables/event.ts | 9 +- backend/src/variables/index.ts | 165 ++------------ backend/src/variables/integration.ts | 105 +++------ backend/src/variables/organization.ts | 12 +- backend/src/variables/permission.ts | 9 +- backend/src/variables/secret.ts | 9 +- backend/src/variables/smtp.ts | 15 +- backend/src/variables/stripe.ts | 9 +- backend/src/variables/token.ts | 15 +- backend/src/variables/user.ts | 6 +- backend/src/variables/workspace.ts | 0 backend/tests/helper/helper.ts | 6 +- 37 files changed, 797 insertions(+), 619 deletions(-) create mode 100644 backend/src/interfaces/utils/crypto.ts create mode 100644 backend/src/interfaces/utils/index.ts delete mode 100644 backend/src/utils/crypto.ts create mode 100644 backend/src/utils/crypto/index.ts create mode 100644 backend/src/utils/setup/backfill.ts create mode 100644 backend/src/utils/setup/index.ts create mode 100644 backend/src/validation/config.ts create mode 100644 backend/src/validation/index.ts create mode 100644 backend/src/variables/crypto.ts delete mode 100644 backend/src/variables/workspace.ts diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 5dce2bc09..326597db3 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,4 +1,5 @@ import InfisicalClient from 'infisical-node'; +import { validateEncryptionKey } from '../validation'; const client = new InfisicalClient({ token: process.env.INFISICAL_TOKEN! @@ -6,7 +7,18 @@ const client = new InfisicalClient({ 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; +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 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/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index 0bf172581..c60b1a18c 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -93,52 +93,8 @@ const markDeletedSecretVersionsHelper = async ({ ); }; -/** - * Initialize secret versioning by setting previously unversioned - * secrets to version 1 and begin populating secret versions. - */ -const initSecretVersioningHelper = 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 addSecretVersionsHelper({ - 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 { takeSecretSnapshotHelper, addSecretVersionsHelper, - markDeletedSecretVersionsHelper, - initSecretVersioningHelper, + markDeletedSecretVersionsHelper }; diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts index e408e848e..e84e35816 100644 --- a/backend/src/ee/services/EESecretService.ts +++ b/backend/src/ee/services/EESecretService.ts @@ -3,8 +3,7 @@ import { ISecretVersion } from '../models'; import { takeSecretSnapshotHelper, addSecretVersionsHelper, - markDeletedSecretVersionsHelper, - initSecretVersioningHelper + markDeletedSecretVersionsHelper } from '../helpers/secret'; import EELicenseService from './EELicenseService'; @@ -64,15 +63,6 @@ class EESecretService { secretIds }); } - - /** - * Initialize secret versioning by setting previously unversioned - * secrets to version 1 and begin populating secret versions. - */ - static async initSecretVersioning() { - if (!EELicenseService.isLicenseValid) return; - await initSecretVersioningHelper(); - } } export default EESecretService; \ No newline at end of file diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 96ad452d9..fe7aba31f 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -13,10 +13,10 @@ import { } from "../models"; import { generateKeyPair, - encryptSymmetric, - decryptSymmetric, - decryptAsymmetric, -} from "../utils/crypto"; + encryptSymmetric128BitHexKeyUTF8, + decryptSymmetric128BitHexKeyUTF8, + decryptAsymmetric +} from '../utils/crypto'; import { SECRET_SHARED, AUTH_MODE_JWT, @@ -26,7 +26,6 @@ import { } from "../variables"; import { getEncryptionKey } from "../config"; import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors"; -import { validateMembership } from "../helpers/membership"; import { validateUserClientForWorkspace } from "../helpers/user"; import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount"; @@ -120,7 +119,7 @@ const createBot = async ({ workspaceId: Types.ObjectId; }) => { const { publicKey, privateKey } = generateKeyPair(); - const { ciphertext, iv, tag } = encryptSymmetric({ + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: privateKey, key: await getEncryptionKey(), }); @@ -161,14 +160,14 @@ const getSecretsHelper = async ({ }); secrets.forEach((secret: ISecret) => { - const secretKey = decryptSymmetric({ + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, key, }); - const secretValue = decryptSymmetric({ + const secretValue = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretValueCiphertext, iv: secret.secretValueIV, tag: secret.secretValueTag, @@ -202,7 +201,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => { if (!bot) throw new Error("Failed to find bot"); if (!bot.isActive) throw new Error("Bot is not active"); - const privateKeyBot = decryptSymmetric({ + const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, @@ -234,7 +233,7 @@ const encryptSymmetricHelper = async ({ plaintext: string; }) => { const key = await getKey({ workspaceId: workspaceId.toString() }); - const { ciphertext, iv, tag } = encryptSymmetric({ + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext, key, }); @@ -266,7 +265,7 @@ const decryptSymmetricHelper = async ({ tag: string; }) => { const key = await getKey({ workspaceId: workspaceId.toString() }); - const plaintext = decryptSymmetric({ + const plaintext = decryptSymmetric128BitHexKeyUTF8({ ciphertext, iv, tag, @@ -281,5 +280,5 @@ export { createBot, getSecretsHelper, encryptSymmetricHelper, - decryptSymmetricHelper, + decryptSymmetricHelper }; diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index 4bfaf1305..fa8c35155 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -1,6 +1,4 @@ import mongoose from 'mongoose'; -import { EESecretService } from '../ee/services'; -import { SecretService } from '../services'; import { getLogger } from '../utils/logger'; /** @@ -21,9 +19,7 @@ const initDatabaseHelper = async ({ mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string'); (await getLogger("database")).info("Database connection established"); - - await EESecretService.initSecretVersioning(); - await SecretService.initSecretBlindIndexDataHelper(); + } catch (err) { (await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`); } diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index c184bad9c..fe39859d8 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -54,9 +54,9 @@ import { } from '../variables'; import crypto from 'crypto'; import * as argon2 from 'argon2'; -import { - encryptSymmetric, - decryptSymmetric +import { + encryptSymmetric128BitHexKeyUTF8, + decryptSymmetric128BitHexKeyUTF8 } from '../utils/crypto'; import { getEncryptionKey } from '../config'; import { TelemetryService } from '../services'; @@ -220,48 +220,6 @@ const validateClientForSecrets = async ({ }); } -/** - * Initialize secret blind index data by setting previously - * un-initialized projects to have secret blind index data - * (Ensures that all projects have associated blind index data) - */ -const initSecretBlindIndexDataHelper = 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 - } = encryptSymmetric({ - plaintext: salt, - key: await getEncryptionKey() - }); - - const secretBlindIndexData = new SecretBlindIndexData({ - workspace: workspaceToBlindIndex, - encryptedSaltCiphertext, - saltIV, - saltTag - }) - - return secretBlindIndexData; - }) - ); - - if (secretBlindIndexDataToInsert.length > 0) { - await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert); - } -} - /** * Create secret blind index data containing encrypted blind index [salt] * for workspace with id [workspaceId] @@ -280,7 +238,7 @@ const createSecretBlindIndexDataHelper = async ({ ciphertext: encryptedSaltCiphertext, iv: saltIV, tag: saltTag - } = encryptSymmetric({ + } = encryptSymmetric128BitHexKeyUTF8({ plaintext: salt, key: await getEncryptionKey() }); @@ -314,7 +272,7 @@ const getSecretBlindIndexSaltHelper = async ({ if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); // decrypt workspace salt - const salt = decryptSymmetric({ + const salt = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secretBlindIndexData.encryptedSaltCiphertext, iv: secretBlindIndexData.saltIV, tag: secretBlindIndexData.saltTag, @@ -376,7 +334,7 @@ const generateSecretBlindIndexHelper = async ({ if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); // decrypt workspace salt - const salt = decryptSymmetric({ + const salt = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secretBlindIndexData.encryptedSaltCiphertext, iv: secretBlindIndexData.saltIV, tag: secretBlindIndexData.saltTag, @@ -934,7 +892,6 @@ const deleteSecretHelper = async ({ export { validateClientForSecret, validateClientForSecrets, - initSecretBlindIndexDataHelper, createSecretBlindIndexDataHelper, getSecretBlindIndexSaltHelper, generateSecretBlindIndexWithSaltHelper, diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 5047a1967..1b6e7737e 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -28,7 +28,6 @@ import { AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY } from '../variables'; -import { encryptSymmetric } from '../utils/crypto'; import { SecretService } from '../services'; /** diff --git a/backend/src/index.ts b/backend/src/index.ts index 7270470ce..e15f330b9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,15 +4,9 @@ dotenv.config(); import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; -import * as Sentry from '@sentry/node'; import { DatabaseService } from './services'; import { setUpHealthEndpoint } from './services/health'; -import { initSmtp } from './services/smtp'; import { TelemetryService } from './services'; -import { setTransporter } from './helpers/nodemailer'; -import { createTestUserForDevelopment } from './utils/addDevelopmentUser'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { patchRouterParam } = require('./utils/patchAsyncRoutes'); import cookieParser from 'cookie-parser'; import swaggerUi = require('swagger-ui-express'); @@ -77,22 +71,13 @@ import { getSiteURL, getSmtpHost } from './config'; +import { setup } from './utils/setup'; const main = async () => { TelemetryService.logTelemetryMessage(); - setTransporter(await initSmtp()); - await DatabaseService.initDatabase(await getMongoURL()); - if ((await getNodeEnv()) !== 'test') { - Sentry.init({ - dsn: await getSentryDSN(), - tracesSampleRate: 1.0, - debug: await getNodeEnv() === 'production' ? false : true, - environment: await getNodeEnv() - }); - } + await setup(); - patchRouterParam(); const app = express(); app.enable('trust proxy'); app.use(express.json()); @@ -176,7 +161,7 @@ const main = async () => { (await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`) }); - await createTestUserForDevelopment(); + // await createTestUserForDevelopment(); setUpHealthEndpoint(server); server.on('close', async () => { diff --git a/backend/src/interfaces/utils/crypto.ts b/backend/src/interfaces/utils/crypto.ts new file mode 100644 index 000000000..cc2c54e3b --- /dev/null +++ b/backend/src/interfaces/utils/crypto.ts @@ -0,0 +1,41 @@ +export interface IGenerateKeyPairOutput { + publicKey: string; + privateKey: string +} + +export interface IEncryptAsymmetricInput { + plaintext: string; + publicKey: string; + privateKey: string; +} + +export interface IEncryptAsymmetricOutput { + ciphertext: string; + nonce: string; +} + +export interface IDecryptAsymmetricInput { + ciphertext: string; + nonce: string; + publicKey: string; + privateKey: string; +} + +export interface IEncryptSymmetricInput { + plaintext: string; + key: string; +} + +export interface IEncryptSymmetricOutput { + ciphertext: string; + iv: string; + tag: string; +} + +export interface IDecryptSymmetricInput { + ciphertext: string; + iv: string; + tag: string; + key: string; +} + diff --git a/backend/src/interfaces/utils/index.ts b/backend/src/interfaces/utils/index.ts new file mode 100644 index 000000000..7d27c9105 --- /dev/null +++ b/backend/src/interfaces/utils/index.ts @@ -0,0 +1 @@ +export * from './crypto'; \ No newline at end of file diff --git a/backend/src/models/backupPrivateKey.ts b/backend/src/models/backupPrivateKey.ts index 70dcd6475..580b0ab38 100644 --- a/backend/src/models/backupPrivateKey.ts +++ b/backend/src/models/backupPrivateKey.ts @@ -1,4 +1,9 @@ import { Schema, model, Types } from 'mongoose'; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 +} from '../variables'; export interface IBackupPrivateKey { _id: Types.ObjectId; @@ -7,6 +12,9 @@ export interface IBackupPrivateKey { iv: string; tag: string; salt: string; + algorithm: string; + keySize: number; + keyEncoding: 'base64' | 'utf8'; verifier: string; } @@ -32,6 +40,24 @@ const backupPrivateKeySchema = new Schema( select: false, required: true }, + algorithm: { // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true + }, + keySize: { // the size of the key used in the algorithm + type: Number, + enum: [256], + required: true + }, + keyEncoding: { + type: String, + enum: [ + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 + ], + required: true + }, salt: { type: String, select: false, diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts index c7e5a9abe..85b1c0adb 100644 --- a/backend/src/models/bot.ts +++ b/backend/src/models/bot.ts @@ -1,4 +1,10 @@ import { Schema, model, Types } from 'mongoose'; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_HEX, + ENCODING_SCHEME_BASE64 +} from '../variables'; export interface IBot { _id: Types.ObjectId; @@ -9,6 +15,9 @@ export interface IBot { encryptedPrivateKey: string; iv: string; tag: string; + algorithm: 'aes-256-gcm'; + keySize: 256; + keyEncoding: 'base64' | 'utf8'; } const botSchema = new Schema( @@ -45,6 +54,24 @@ const botSchema = new Schema( type: String, required: true, select: false + }, + algorithm: { // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true + }, + keySize: { // the size of the key used in the algorithm + type: Number, + enum: [256], + required: true + }, + keyEncoding: { + type: String, + enum: [ + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 + ], + required: true } }, { diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index ead969fc7..b55aa9b2f 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -14,6 +14,9 @@ import { INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 } from "../variables"; export interface IIntegrationAuth extends Document { @@ -31,6 +34,9 @@ export interface IIntegrationAuth extends Document { accessCiphertext?: string; accessIV?: string; accessTag?: string; + algorithm?: 'aes-256-gcm'; + keySize?: 256; + keyEncoding: 'utf8' | 'base64'; accessExpiresAt?: Date; } @@ -109,6 +115,24 @@ const integrationAuthSchema = new Schema( type: Date, select: false, }, + algorithm: { // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true + }, + keySize: { // the size of the key used in the algorithm + type: Number, + enum: [256], + required: true + }, + keyEncoding: { + type: String, + enum: [ + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 + ], + required: true + } }, { timestamps: true, diff --git a/backend/src/models/secretBlindIndexData.ts b/backend/src/models/secretBlindIndexData.ts index 47649dde4..47e82c053 100644 --- a/backend/src/models/secretBlindIndexData.ts +++ b/backend/src/models/secretBlindIndexData.ts @@ -1,4 +1,9 @@ import { Schema, model, Types, Document } from 'mongoose'; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 +} from '../variables'; export interface ISecretBlindIndexData extends Document { _id: Types.ObjectId; @@ -6,6 +11,9 @@ export interface ISecretBlindIndexData extends Document { encryptedSaltCiphertext: string; saltIV: string; saltTag: string; + algorithm: 'aes-256-gcm'; + keySize: 256; + keyEncoding: 'base64' | 'utf8' } const secretBlindIndexDataSchema = new Schema( @@ -26,7 +34,26 @@ const secretBlindIndexDataSchema = new Schema( saltTag: { type: String, required: true + }, + algorithm: { + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true + }, + keySize: { + type: Number, + enum: [256], + required: true + }, + keyEncoding: { + type: String, + enum: [ + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 + ], + required: true } + } ); diff --git a/backend/src/services/SecretService.ts b/backend/src/services/SecretService.ts index c215431f1..5b5b73e5f 100644 --- a/backend/src/services/SecretService.ts +++ b/backend/src/services/SecretService.ts @@ -1,4 +1,3 @@ -// WIP import { Types } from 'mongoose'; import { ISecret @@ -11,7 +10,6 @@ import { DeleteSecretParams } from '../interfaces/services/SecretService'; import { - initSecretBlindIndexDataHelper, createSecretBlindIndexDataHelper, getSecretBlindIndexSaltHelper, generateSecretBlindIndexWithSaltHelper, @@ -24,16 +22,6 @@ import { } from '../helpers/secrets'; class SecretService { - - /** - * - * @param param0 h - * @returns - */ - - static async initSecretBlindIndexDataHelper() { - return await initSecretBlindIndexDataHelper(); - } /** * Create secret blind index data containing encrypted blind index salt diff --git a/backend/src/utils/crypto.ts b/backend/src/utils/crypto.ts deleted file mode 100644 index ec61b496e..000000000 --- a/backend/src/utils/crypto.ts +++ /dev/null @@ -1,139 +0,0 @@ -import nacl from 'tweetnacl'; -import util from 'tweetnacl-util'; -import AesGCM from './aes-gcm'; - -/** - * Return new base64, NaCl, public-private key pair. - * @returns {Object} obj - * @returns {String} obj.publicKey - base64, NaCl, public key - * @returns {String} obj.privateKey - base64, NaCl, private key - */ -const generateKeyPair = () => { - const pair = nacl.box.keyPair(); - - return ({ - publicKey: util.encodeBase64(pair.publicKey), - privateKey: util.encodeBase64(pair.secretKey) - }); -} - -/** - * Return assymmetrically encrypted [plaintext] using [publicKey] where - * [publicKey] likely belongs to the recipient. - * @param {Object} obj - * @param {String} obj.plaintext - plaintext to encrypt - * @param {String} obj.publicKey - public key of the recipient - * @param {String} obj.privateKey - private key of the sender (current user) - * @returns {Object} obj - * @returns {String} ciphertext - base64-encoded ciphertext - * @returns {String} nonce - base64-encoded nonce - */ -const encryptAsymmetric = ({ - plaintext, - publicKey, - privateKey -}: { - plaintext: string; - publicKey: string; - privateKey: string; -}) => { - const nonce = nacl.randomBytes(24); - const ciphertext = nacl.box( - util.decodeUTF8(plaintext), - nonce, - util.decodeBase64(publicKey), - util.decodeBase64(privateKey) - ); - - return { - ciphertext: util.encodeBase64(ciphertext), - nonce: util.encodeBase64(nonce) - }; -}; - -/** - * Return assymmetrically decrypted [ciphertext] using [privateKey] where - * [privateKey] likely belongs to the recipient. - * @param {Object} obj - * @param {String} obj.ciphertext - ciphertext to decrypt - * @param {String} obj.nonce - nonce - * @param {String} obj.publicKey - public key of the sender - * @param {String} obj.privateKey - private key of the receiver (current user) - * @param {String} plaintext - UTF8 plaintext - */ -const decryptAsymmetric = ({ - ciphertext, - nonce, - publicKey, - privateKey -}: { - ciphertext: string; - nonce: string; - publicKey: string; - privateKey: string; -}): string => { - const plaintext: any = nacl.box.open( - util.decodeBase64(ciphertext), - util.decodeBase64(nonce), - util.decodeBase64(publicKey), - util.decodeBase64(privateKey) - ); - - return util.encodeUTF8(plaintext); -}; - -/** - * Return symmetrically encrypted [plaintext] using [key]. - * @param {Object} obj - * @param {String} obj.plaintext - plaintext to encrypt - * @param {String} obj.key - hex key - */ -const encryptSymmetric = ({ - plaintext, - key -}: { - plaintext: string; - key: string; -}) => { - const obj = AesGCM.encrypt(plaintext, key); - const { ciphertext, iv, tag } = obj; - - return { - ciphertext, - iv, - tag - }; -}; - -/** - * Return symmetrically decypted [ciphertext] using [iv], [tag], - * and [key]. - * @param {Object} obj - * @param {String} obj.ciphertext - ciphertext to decrypt - * @param {String} obj.iv - iv - * @param {String} obj.tag - tag - * @param {String} obj.key - hex key - * - */ -const decryptSymmetric = ({ - ciphertext, - iv, - tag, - key -}: { - ciphertext: string; - iv: string; - tag: string; - key: string; -}): string => { - const plaintext = AesGCM.decrypt(ciphertext, iv, tag, key); - return plaintext; -}; - -export { - generateKeyPair, - encryptAsymmetric, - decryptAsymmetric, - encryptSymmetric, - decryptSymmetric -}; diff --git a/backend/src/utils/crypto/index.ts b/backend/src/utils/crypto/index.ts new file mode 100644 index 000000000..bfe9df85a --- /dev/null +++ b/backend/src/utils/crypto/index.ts @@ -0,0 +1,237 @@ +import crypto from 'crypto'; +import nacl from 'tweetnacl'; +import util from 'tweetnacl-util'; +import { + IGenerateKeyPairOutput, + IEncryptAsymmetricInput, + IEncryptAsymmetricOutput, + IDecryptAsymmetricInput, + IEncryptSymmetricInput, + IEncryptSymmetricOutput, + IDecryptSymmetricInput +} from '../../interfaces/utils'; +import { + BadRequestError, + InternalServerError +} from '../errors'; +import { + ALGORITHM_AES_256_GCM, + BLOCK_SIZE_BYTES_32, + BLOCK_SIZE_BYTES_16 +} from '../../variables'; +import { validateEncryptionKey } from '../../validation'; + +/** + * Return new base64, NaCl, public-private key pair. + * @returns {Object} obj + * @returns {String} obj.publicKey - (base64) NaCl, public key + * @returns {String} obj.privateKey - (base64), NaCl, private key + */ +const generateKeyPair = (): IGenerateKeyPairOutput => { + const pair = nacl.box.keyPair(); + + return ({ + publicKey: util.encodeBase64(pair.publicKey), + privateKey: util.encodeBase64(pair.secretKey) + }); +} + +/** + * Return assymmetrically encrypted [plaintext] using [publicKey] where + * [publicKey] likely belongs to the recipient. + * @param {Object} obj + * @param {String} obj.plaintext - plaintext to encrypt + * @param {String} obj.publicKey - (base64) Nacl public key of the recipient + * @param {String} obj.privateKey - (base64) Nacl private key of the sender (current user) + * @returns {Object} obj + * @returns {String} obj.ciphertext - (base64) ciphertext + * @returns {String} obj.nonce - (base64) nonce + */ +const encryptAsymmetric = ({ + plaintext, + publicKey, + privateKey +}: IEncryptAsymmetricInput): IEncryptAsymmetricOutput => { + const nonce = nacl.randomBytes(24); + const ciphertext = nacl.box( + util.decodeUTF8(plaintext), + nonce, + util.decodeBase64(publicKey), + util.decodeBase64(privateKey) + ); + + return { + ciphertext: util.encodeBase64(ciphertext), + nonce: util.encodeBase64(nonce) + }; +}; + +/** + * Return assymmetrically decrypted [ciphertext] using [privateKey] where + * [privateKey] likely belongs to the recipient. + * @param {Object} obj + * @param {String} obj.ciphertext - ciphertext to decrypt + * @param {String} obj.nonce - (base64) nonce + * @param {String} obj.publicKey - (base64) public key of the sender + * @param {String} obj.privateKey - (base64) private key of the receiver (current user) + * @returns {String} plaintext - (utf8) plaintext + */ +const decryptAsymmetric = ({ + ciphertext, + nonce, + publicKey, + privateKey +}: IDecryptAsymmetricInput): string => { + const plaintext: Uint8Array | null = nacl.box.open( + util.decodeBase64(ciphertext), + util.decodeBase64(nonce), + util.decodeBase64(publicKey), + util.decodeBase64(privateKey) + ); + + if (plaintext == null) throw BadRequestError({ + message: 'Invalid ciphertext or keys' + }); + + 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(BLOCK_SIZE_BYTES_32); + 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]. + * + * NOTE: THIS FUNCTION SHOULD NOT BE USED FOR ALL FUTURE + * ENCRYPTION OPERATIONS UNLESS IT TOUCHES OLD FUNCTIONALITY + * THAT USES IT. USE encryptSymmetric() instead + * + * @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 encryptSymmetric128BitHexKeyUTF8 = ({ + plaintext, + key +}: IEncryptSymmetricInput) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16); + const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, key, 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]. + * + * NOTE: THIS FUNCTION SHOULD NOT BE USED FOR ALL FUTURE + * DECRYPTION OPERATIONS UNLESS IT TOUCHES OLD FUNCTIONALITY + * THAT USES IT. USE decryptSymmetric() instead + * + * @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 - (hex) 128-bit key + * @returns {String} cleartext - the deciphered ciphertext + */ +const decryptSymmetric128BitHexKeyUTF8 = ({ + ciphertext, + iv, + tag, + key +}: IDecryptSymmetricInput) => { + const decipher = crypto.createDecipheriv( + ALGORITHM_AES_256_GCM, + key, + Buffer.from(iv, 'base64') + ); + + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); + + return cleartext; +} + +export { + generateKeyPair, + encryptAsymmetric, + decryptAsymmetric, + encryptSymmetric, + decryptSymmetric, + encryptSymmetric128BitHexKeyUTF8, + decryptSymmetric128BitHexKeyUTF8 +}; diff --git a/backend/src/utils/setup/backfill.ts b/backend/src/utils/setup/backfill.ts new file mode 100644 index 000000000..4a06fe9ee --- /dev/null +++ b/backend/src/utils/setup/backfill.ts @@ -0,0 +1,210 @@ +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, + keySize: 256, + 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, + keySize: 256, + 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, + keySize: 256, + keyEncoding: ENCODING_SCHEME_UTF8 + } + } + ); + + // backfill integration auth encryption metadata + await IntegrationAuth.updateMany( + { + + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + + } + } + ); + + // 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/index.ts b/backend/src/utils/setup/index.ts new file mode 100644 index 000000000..059edf6f6 --- /dev/null +++ b/backend/src/utils/setup/index.ts @@ -0,0 +1,51 @@ +import * as Sentry from '@sentry/node'; +import { DatabaseService } 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 { + backfillSecretVersions, + backfillSecretBlindIndexData, + backfillEncryptionMetadata +} from './backfill'; +import { + getNodeEnv, + getMongoURL, + getSentryDSN +} from '../../config'; + +/** + * Prepare Infisical upon startup. This includes tasks like: + * - Initializing SMTP configuration + * - Initializing the database connection + * - Initializing Sentry + * - Backfilling data + */ +export const setup = async () => { + // initializing SMTP configuration + setTransporter(await initSmtp()); + + // initializing the database connection + await DatabaseService.initDatabase(await getMongoURL()); + + // backfilling data + await backfillSecretVersions(); + 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() + }); + } + + patchRouterParam(); + await createTestUserForDevelopment(); +} + diff --git a/backend/src/validation/config.ts b/backend/src/validation/config.ts new file mode 100644 index 000000000..c96164929 --- /dev/null +++ b/backend/src/validation/config.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..de8bde7ba --- /dev/null +++ b/backend/src/validation/index.ts @@ -0,0 +1 @@ +export * from './config'; \ No newline at end of file diff --git a/backend/src/variables/action.ts b/backend/src/variables/action.ts index 682b357a7..9d4fb18d9 100644 --- a/backend/src/variables/action.ts +++ b/backend/src/variables/action.ts @@ -1,15 +1,6 @@ -const ACTION_LOGIN = 'login'; -const ACTION_LOGOUT = 'logout'; -const ACTION_ADD_SECRETS = 'addSecrets'; -const ACTION_DELETE_SECRETS = 'deleteSecrets'; -const ACTION_UPDATE_SECRETS = 'updateSecrets'; -const ACTION_READ_SECRETS = 'readSecrets'; - -export { - ACTION_LOGIN, - ACTION_LOGOUT, - ACTION_ADD_SECRETS, - ACTION_DELETE_SECRETS, - ACTION_UPDATE_SECRETS, - ACTION_READ_SECRETS -} \ No newline at end of file +export const ACTION_LOGIN = 'login'; +export const ACTION_LOGOUT = 'logout'; +export const ACTION_ADD_SECRETS = 'addSecrets'; +export const ACTION_DELETE_SECRETS = 'deleteSecrets'; +export const ACTION_UPDATE_SECRETS = 'updateSecrets'; +export const ACTION_READ_SECRETS = 'readSecrets'; \ No newline at end of file diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index 2fe1f4fc6..ac19d2756 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -1,11 +1,4 @@ -const AUTH_MODE_JWT = 'jwt'; -const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount'; -const AUTH_MODE_SERVICE_TOKEN = 'serviceToken'; -const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate - -export { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} \ No newline at end of file +export const AUTH_MODE_JWT = 'jwt'; +export const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount'; +export const AUTH_MODE_SERVICE_TOKEN = 'serviceToken'; +export const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate \ No newline at end of file diff --git a/backend/src/variables/crypto.ts b/backend/src/variables/crypto.ts new file mode 100644 index 000000000..3b8c7820d --- /dev/null +++ b/backend/src/variables/crypto.ts @@ -0,0 +1,7 @@ +export const ALGORITHM_AES_256_GCM = 'aes-256-gcm'; +export const BLOCK_SIZE_BYTES_32 = 32; +export const BLOCK_SIZE_BYTES_16 = 16; + +export const ENCODING_SCHEME_UTF8 = 'utf8'; +export const ENCODING_SCHEME_HEX = 'hex'; +export const ENCODING_SCHEME_BASE64 = 'base64'; \ No newline at end of file diff --git a/backend/src/variables/environment.ts b/backend/src/variables/environment.ts index 44d7cdbb2..b068b3d36 100644 --- a/backend/src/variables/environment.ts +++ b/backend/src/variables/environment.ts @@ -1,14 +1,6 @@ // environments -const ENV_DEV = 'dev'; -const ENV_TESTING = 'test'; -const ENV_STAGING = 'staging'; -const ENV_PROD = 'prod'; -const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]); - -export { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET -} \ No newline at end of file +export const ENV_DEV = 'dev'; +export const ENV_TESTING = 'test'; +export const ENV_STAGING = 'staging'; +export const ENV_PROD = 'prod'; +export const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]); \ No newline at end of file diff --git a/backend/src/variables/event.ts b/backend/src/variables/event.ts index 4477e8e02..1c55eb3ec 100644 --- a/backend/src/variables/event.ts +++ b/backend/src/variables/event.ts @@ -1,7 +1,2 @@ -const EVENT_PUSH_SECRETS = 'pushSecrets'; -const EVENT_PULL_SECRETS = 'pullSecrets'; - -export { - EVENT_PUSH_SECRETS, - EVENT_PULL_SECRETS -} \ No newline at end of file +export const EVENT_PUSH_SECRETS = 'pushSecrets'; +export const EVENT_PULL_SECRETS = 'pullSecrets'; \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 979de21ab..5695bd547 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -1,152 +1,13 @@ -import { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET, -} from "./environment"; -import { - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_AZURE_TOKEN_URL, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_GITLAB_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_GITLAB_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_RAILWAY_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_CIRCLECI_API_URL, - INTEGRATION_TRAVISCI_API_URL, - INTEGRATION_SUPABASE_API_URL, - getIntegrationOptions -} from "./integration"; -import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; -import { SECRET_SHARED, SECRET_PERSONAL } from "./secret"; -import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event"; -import { - ACTION_LOGIN, - ACTION_LOGOUT, - ACTION_ADD_SECRETS, - ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS, - ACTION_READ_SECRETS -} from './action'; -import { - SMTP_HOST_SENDGRID, - SMTP_HOST_MAILGUN, - SMTP_HOST_SOCKETLABS, - SMTP_HOST_ZOHOMAIL -} from './smtp'; -import { PLAN_STARTER, PLAN_PRO } from './stripe'; -import { - MFA_METHOD_EMAIL -} from './user'; -import { - TOKEN_EMAIL_CONFIRMATION, - TOKEN_EMAIL_MFA, - TOKEN_EMAIL_ORG_INVITATION, - TOKEN_EMAIL_PASSWORD_RESET -} from './token'; -import { - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS -} from './permission'; -import { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from './authentication'; - -export { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, - SECRET_SHARED, - SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET, - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_AZURE_TOKEN_URL, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_GITLAB_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_GITLAB_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_RAILWAY_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_CIRCLECI_API_URL, - INTEGRATION_TRAVISCI_API_URL, - INTEGRATION_SUPABASE_API_URL, - EVENT_PUSH_SECRETS, - EVENT_PULL_SECRETS, - ACTION_LOGIN, - ACTION_LOGOUT, - ACTION_ADD_SECRETS, - ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS, - ACTION_READ_SECRETS, - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, - getIntegrationOptions, - SMTP_HOST_SENDGRID, - SMTP_HOST_MAILGUN, - SMTP_HOST_SOCKETLABS, - SMTP_HOST_ZOHOMAIL, - PLAN_STARTER, - PLAN_PRO, - MFA_METHOD_EMAIL, - TOKEN_EMAIL_CONFIRMATION, - TOKEN_EMAIL_MFA, - TOKEN_EMAIL_ORG_INVITATION, - TOKEN_EMAIL_PASSWORD_RESET, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -}; +export * from './action'; +export * from './authentication'; +export * from './crypto'; +export * from './environment'; +export * from './event'; +export * from './integration'; +export * from './organization'; +export * from './permission'; +export * from './secret'; +export * from './smtp'; +export * from './stripe'; +export * from './token'; +export * from './user'; \ No newline at end of file diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index aac78968d..6efd96817 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -8,21 +8,21 @@ import { } from '../config'; // integrations -const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; -const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store'; -const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager'; -const INTEGRATION_HEROKU = "heroku"; -const INTEGRATION_VERCEL = "vercel"; -const INTEGRATION_NETLIFY = "netlify"; -const INTEGRATION_GITHUB = "github"; -const INTEGRATION_GITLAB = "gitlab"; -const INTEGRATION_RENDER = "render"; -const INTEGRATION_RAILWAY = "railway"; -const INTEGRATION_FLYIO = "flyio"; -const INTEGRATION_CIRCLECI = "circleci"; -const INTEGRATION_TRAVISCI = "travisci"; -const INTEGRATION_SUPABASE = 'supabase'; -const INTEGRATION_SET = new Set([ +export const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; +export const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store'; +export const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager'; +export const INTEGRATION_HEROKU = "heroku"; +export const INTEGRATION_VERCEL = "vercel"; +export const INTEGRATION_NETLIFY = "netlify"; +export const INTEGRATION_GITHUB = "github"; +export const INTEGRATION_GITLAB = "gitlab"; +export const INTEGRATION_RENDER = "render"; +export const INTEGRATION_RAILWAY = "railway"; +export const INTEGRATION_FLYIO = "flyio"; +export const INTEGRATION_CIRCLECI = "circleci"; +export const INTEGRATION_TRAVISCI = "travisci"; +export const INTEGRATION_SUPABASE = 'supabase'; +export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, INTEGRATION_VERCEL, @@ -37,31 +37,31 @@ const INTEGRATION_SET = new Set([ ]); // integration types -const INTEGRATION_OAUTH2 = "oauth2"; +export const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints -const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`; -const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; -const INTEGRATION_VERCEL_TOKEN_URL = +export const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`; +export const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; +export const INTEGRATION_VERCEL_TOKEN_URL = "https://api.vercel.com/v2/oauth/access_token"; -const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; -const INTEGRATION_GITHUB_TOKEN_URL = +export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; +export const INTEGRATION_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; -const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; +export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; // integration apps endpoints -const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; -const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; -const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; -const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; -const INTEGRATION_RENDER_API_URL = "https://api.render.com"; -const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2"; -const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; -const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; -const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; -const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; +export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; +export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; +export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; +export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; +export const INTEGRATION_RENDER_API_URL = "https://api.render.com"; +export const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2"; +export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; +export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; +export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; +export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; -const getIntegrationOptions = async () => { +export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ { name: 'Heroku', @@ -202,41 +202,4 @@ const getIntegrationOptions = async () => { ] return INTEGRATION_OPTIONS; -} - - -export { - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_AZURE_TOKEN_URL, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_GITLAB_API_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_GITLAB_TOKEN_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_RAILWAY_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_CIRCLECI_API_URL, - INTEGRATION_TRAVISCI_API_URL, - INTEGRATION_SUPABASE_API_URL, - getIntegrationOptions -}; +} \ No newline at end of file diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 80c7102c1..4f5620236 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -1,12 +1,10 @@ // membership roles -const OWNER = "owner"; -const ADMIN = "admin"; -const MEMBER = "member"; +export const OWNER = "owner"; +export const ADMIN = "admin"; +export const MEMBER = "member"; // membership statuses -const INVITED = "invited"; +export const INVITED = "invited"; // -- organization -const ACCEPTED = "accepted"; - -export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED }; +export const ACCEPTED = "accepted"; \ No newline at end of file diff --git a/backend/src/variables/permission.ts b/backend/src/variables/permission.ts index 769344d7f..98c9ef538 100644 --- a/backend/src/variables/permission.ts +++ b/backend/src/variables/permission.ts @@ -1,7 +1,2 @@ -const PERMISSION_READ_SECRETS = 'read'; -const PERMISSION_WRITE_SECRETS = 'write'; - -export { - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS -} \ No newline at end of file +export const PERMISSION_READ_SECRETS = 'read'; +export const PERMISSION_WRITE_SECRETS = 'write'; \ No newline at end of file diff --git a/backend/src/variables/secret.ts b/backend/src/variables/secret.ts index 31cbcf951..571e66b9d 100644 --- a/backend/src/variables/secret.ts +++ b/backend/src/variables/secret.ts @@ -1,8 +1,3 @@ // secrets -const SECRET_SHARED = 'shared'; -const SECRET_PERSONAL = 'personal'; - -export { - SECRET_SHARED, - SECRET_PERSONAL -} \ No newline at end of file +export const SECRET_SHARED = 'shared'; +export const SECRET_PERSONAL = 'personal'; \ No newline at end of file diff --git a/backend/src/variables/smtp.ts b/backend/src/variables/smtp.ts index a88f229bd..5b4dbd191 100644 --- a/backend/src/variables/smtp.ts +++ b/backend/src/variables/smtp.ts @@ -1,11 +1,4 @@ -const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net'; -const SMTP_HOST_MAILGUN = 'smtp.mailgun.org'; -const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com'; -const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com'; - -export { - SMTP_HOST_SENDGRID, - SMTP_HOST_MAILGUN, - SMTP_HOST_SOCKETLABS, - SMTP_HOST_ZOHOMAIL -} \ No newline at end of file +export const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net'; +export const SMTP_HOST_MAILGUN = 'smtp.mailgun.org'; +export const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com'; +export const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com'; \ No newline at end of file diff --git a/backend/src/variables/stripe.ts b/backend/src/variables/stripe.ts index ecdbd98ae..7b6ae8fa1 100644 --- a/backend/src/variables/stripe.ts +++ b/backend/src/variables/stripe.ts @@ -1,7 +1,2 @@ -const PLAN_STARTER = 'starter'; -const PLAN_PRO = 'pro'; - -export { - PLAN_STARTER, - PLAN_PRO -} \ No newline at end of file +export const PLAN_STARTER = 'starter'; +export const PLAN_PRO = 'pro'; \ No newline at end of file diff --git a/backend/src/variables/token.ts b/backend/src/variables/token.ts index ecb63990f..2ced95d9c 100644 --- a/backend/src/variables/token.ts +++ b/backend/src/variables/token.ts @@ -1,11 +1,4 @@ -const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation'; -const TOKEN_EMAIL_MFA = 'emailMfa'; -const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation'; -const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset'; - -export { - TOKEN_EMAIL_CONFIRMATION, - TOKEN_EMAIL_MFA, - TOKEN_EMAIL_ORG_INVITATION, - TOKEN_EMAIL_PASSWORD_RESET -} \ No newline at end of file +export const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation'; +export const TOKEN_EMAIL_MFA = 'emailMfa'; +export const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation'; +export const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset'; \ No newline at end of file diff --git a/backend/src/variables/user.ts b/backend/src/variables/user.ts index baa27d35d..7e5b53c4b 100644 --- a/backend/src/variables/user.ts +++ b/backend/src/variables/user.ts @@ -1,5 +1 @@ -const MFA_METHOD_EMAIL = 'email'; - -export { - MFA_METHOD_EMAIL -} \ No newline at end of file +export const MFA_METHOD_EMAIL = 'email'; \ No newline at end of file diff --git a/backend/src/variables/workspace.ts b/backend/src/variables/workspace.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/tests/helper/helper.ts b/backend/tests/helper/helper.ts index c59c8d43b..d72a101c1 100644 --- a/backend/tests/helper/helper.ts +++ b/backend/tests/helper/helper.ts @@ -10,7 +10,9 @@ const jsrp = require('jsrp'); // eslint-disable-next-line @typescript-eslint/no-var-requires const axios = require('axios'); import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser"; -import { encryptSymmetric } from "../../src/utils/crypto"; +import { + encryptSymmetric128BitHexKeyUTF8 +} from '../../src/utils/crypto'; interface TokenData { token: string; @@ -64,7 +66,7 @@ export const getJWTFromTestUser = (): Promise => { export const getServiceTokenFromTestUser = async () => { const loggedInUserDetails = await getJWTFromTestUser() const randomBytes = crypto.randomBytes(16).toString('hex'); - const { ciphertext, iv, tag } = encryptSymmetric({ + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: plainTextWorkspaceKey, key: randomBytes, }); From 5eeda6272c3a983e51ca9421ea82c52d558851a7 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 4 May 2023 20:35:06 +0300 Subject: [PATCH 02/22] Checkpoint adding crypto metadata --- backend/src/controllers/v2/secretController.ts | 10 +++++++--- backend/src/models/backupPrivateKey.ts | 6 ------ backend/src/models/bot.ts | 6 ------ backend/src/models/integrationAuth.ts | 8 +------- backend/src/models/secret.ts | 18 ++++++++++++++++++ backend/src/models/secretBlindIndexData.ts | 6 ------ backend/src/utils/crypto/index.ts | 6 +++--- backend/src/utils/setup/backfill.ts | 15 ++++++++++----- backend/src/variables/crypto.ts | 2 +- 9 files changed, 40 insertions(+), 37 deletions(-) diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index 75eff3127..fecba43d6 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -6,7 +6,7 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre const { ValidationError } = mongoose.Error; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { AnyBulkWriteOperation } from 'mongodb'; -import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; +import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; import { TelemetryService } from '../../services'; import { User } from "../../models"; import { AccountNotFoundError } from '../../utils/errors'; @@ -36,7 +36,9 @@ export const createSecret = async (req: Request, res: Response) => { workspace: new Types.ObjectId(workspaceId), environment, type: secretToCreate.type, - user: new Types.ObjectId(req.user._id) + user: new Types.ObjectId(req.user._id), + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 } @@ -92,7 +94,9 @@ export const createSecrets = async (req: Request, res: Response) => { workspace: new Types.ObjectId(workspaceId), environment, type: rawSecret.type, - user: new Types.ObjectId(req.user._id) + user: new Types.ObjectId(req.user._id), + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 } sanitizedSecretesToCreate.push(safeUpdateFields) diff --git a/backend/src/models/backupPrivateKey.ts b/backend/src/models/backupPrivateKey.ts index 580b0ab38..09bcbb588 100644 --- a/backend/src/models/backupPrivateKey.ts +++ b/backend/src/models/backupPrivateKey.ts @@ -13,7 +13,6 @@ export interface IBackupPrivateKey { tag: string; salt: string; algorithm: string; - keySize: number; keyEncoding: 'base64' | 'utf8'; verifier: string; } @@ -45,11 +44,6 @@ const backupPrivateKeySchema = new Schema( enum: [ALGORITHM_AES_256_GCM], required: true }, - keySize: { // the size of the key used in the algorithm - type: Number, - enum: [256], - required: true - }, keyEncoding: { type: String, enum: [ diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts index 85b1c0adb..3dd90fb07 100644 --- a/backend/src/models/bot.ts +++ b/backend/src/models/bot.ts @@ -16,7 +16,6 @@ export interface IBot { iv: string; tag: string; algorithm: 'aes-256-gcm'; - keySize: 256; keyEncoding: 'base64' | 'utf8'; } @@ -60,11 +59,6 @@ const botSchema = new Schema( enum: [ALGORITHM_AES_256_GCM], required: true }, - keySize: { // the size of the key used in the algorithm - type: Number, - enum: [256], - required: true - }, keyEncoding: { type: String, enum: [ diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index b55aa9b2f..affe811df 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -35,8 +35,7 @@ export interface IIntegrationAuth extends Document { accessIV?: string; accessTag?: string; algorithm?: 'aes-256-gcm'; - keySize?: 256; - keyEncoding: 'utf8' | 'base64'; + keyEncoding?: 'utf8' | 'base64'; accessExpiresAt?: Date; } @@ -120,11 +119,6 @@ const integrationAuthSchema = new Schema( enum: [ALGORITHM_AES_256_GCM], required: true }, - keySize: { // the size of the key used in the algorithm - type: Number, - enum: [256], - required: true - }, keyEncoding: { type: String, enum: [ diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 7670124bf..39e362bf7 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.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'; import { ROOT_FOLDER_PATH } from '../utils/folder'; @@ -25,6 +28,8 @@ export interface ISecret { secretCommentIV?: string; secretCommentTag?: string; secretCommentHash?: string; + algorithm: 'aes-256-gcm'; + keyEncoding: 'utf8' | 'base64'; tags?: string[]; path?: string; folder?: Types.ObjectId; @@ -111,6 +116,19 @@ const secretSchema = new Schema( type: String, required: false }, + 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 + }, // the full path to the secret in relation to folders path: { type: String, diff --git a/backend/src/models/secretBlindIndexData.ts b/backend/src/models/secretBlindIndexData.ts index 47e82c053..fc9896618 100644 --- a/backend/src/models/secretBlindIndexData.ts +++ b/backend/src/models/secretBlindIndexData.ts @@ -12,7 +12,6 @@ export interface ISecretBlindIndexData extends Document { saltIV: string; saltTag: string; algorithm: 'aes-256-gcm'; - keySize: 256; keyEncoding: 'base64' | 'utf8' } @@ -40,11 +39,6 @@ const secretBlindIndexDataSchema = new Schema( enum: [ALGORITHM_AES_256_GCM], required: true }, - keySize: { - type: Number, - enum: [256], - required: true - }, keyEncoding: { type: String, enum: [ diff --git a/backend/src/utils/crypto/index.ts b/backend/src/utils/crypto/index.ts index bfe9df85a..0e7fb24b3 100644 --- a/backend/src/utils/crypto/index.ts +++ b/backend/src/utils/crypto/index.ts @@ -16,7 +16,7 @@ import { } from '../errors'; import { ALGORITHM_AES_256_GCM, - BLOCK_SIZE_BYTES_32, + NONCE_BYTES_SIZE, BLOCK_SIZE_BYTES_16 } from '../../variables'; import { validateEncryptionKey } from '../../validation'; @@ -112,7 +112,7 @@ const encryptSymmetric = ({ }: IEncryptSymmetricInput): IEncryptSymmetricOutput => { validateEncryptionKey(key); - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_32); + const iv = crypto.randomBytes(NONCE_BYTES_SIZE); const secretKey = crypto.createSecretKey(key, 'base64'); const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, secretKey, iv); @@ -169,7 +169,7 @@ const decryptSymmetric = ({ * * @param {Object} obj * @param {String} obj.plaintext - (utf8) plaintext to encrypt - * @param {String} obj.key - (base64) 256-bit key + * @param {String} obj.key - (hex) 128-bit key * @returns {Object} obj * @returns {String} obj.ciphertext (base64) ciphertext * @returns {String} obj.iv (base64) iv diff --git a/backend/src/utils/setup/backfill.ts b/backend/src/utils/setup/backfill.ts index 4a06fe9ee..8a964dfd7 100644 --- a/backend/src/utils/setup/backfill.ts +++ b/backend/src/utils/setup/backfill.ts @@ -114,7 +114,6 @@ export const backfillEncryptionMetadata = async () => { { $set: { algorithm: ALGORITHM_AES_256_GCM, - keySize: 256, keyEncoding: ENCODING_SCHEME_UTF8 } } @@ -136,7 +135,6 @@ export const backfillEncryptionMetadata = async () => { { $set: { algorithm: ALGORITHM_AES_256_GCM, - keySize: 256, keyEncoding: ENCODING_SCHEME_UTF8 } } @@ -158,7 +156,6 @@ export const backfillEncryptionMetadata = async () => { { $set: { algorithm: ALGORITHM_AES_256_GCM, - keySize: 256, keyEncoding: ENCODING_SCHEME_UTF8 } } @@ -167,12 +164,20 @@ export const backfillEncryptionMetadata = async () => { // 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 } } ); diff --git a/backend/src/variables/crypto.ts b/backend/src/variables/crypto.ts index 3b8c7820d..bd2ae110c 100644 --- a/backend/src/variables/crypto.ts +++ b/backend/src/variables/crypto.ts @@ -1,5 +1,5 @@ export const ALGORITHM_AES_256_GCM = 'aes-256-gcm'; -export const BLOCK_SIZE_BYTES_32 = 32; +export const NONCE_BYTES_SIZE = 12; export const BLOCK_SIZE_BYTES_16 = 16; export const ENCODING_SCHEME_UTF8 = 'utf8'; From 87e047a1520ae56ee2210c77cbfba50bf36a6631 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 6 May 2023 22:07:59 +0300 Subject: [PATCH 03/22] 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(() => { From f3dc7fcf7b9b6936599269c93a192cae02c8a893 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 24 May 2023 10:46:39 -0400 Subject: [PATCH 04/22] add timout to pull requests --- .github/workflows/check-be-pull-request.yml | 1 + .github/workflows/check-fe-pull-request.yml | 29 +++++++++------------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/workflows/check-be-pull-request.yml b/.github/workflows/check-be-pull-request.yml index 3cb010e8c..d2c2178fb 100644 --- a/.github/workflows/check-be-pull-request.yml +++ b/.github/workflows/check-be-pull-request.yml @@ -13,6 +13,7 @@ jobs: check-be-pr: name: Check runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: ☁️ Checkout source diff --git a/.github/workflows/check-fe-pull-request.yml b/.github/workflows/check-fe-pull-request.yml index b91e6f060..75465a014 100644 --- a/.github/workflows/check-fe-pull-request.yml +++ b/.github/workflows/check-fe-pull-request.yml @@ -2,40 +2,35 @@ name: Check Frontend Pull Request on: pull_request: - types: [ opened, synchronize ] + types: [opened, synchronize] paths: - - 'frontend/**' - - '!frontend/README.md' - - '!frontend/.*' - - 'frontend/.eslintrc.js' - + - "frontend/**" + - "!frontend/README.md" + - "!frontend/.*" + - "frontend/.eslintrc.js" jobs: - check-fe-pr: name: Check runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - - name: ☁️ Checkout source + - name: ☁️ Checkout source uses: actions/checkout@v3 - - - name: 🔧 Setup Node 16 + - name: 🔧 Setup Node 16 uses: actions/setup-node@v3 with: - node-version: '16' - cache: 'npm' + node-version: "16" + cache: "npm" cache-dependency-path: frontend/package-lock.json - - - name: 📦 Install dependencies + - name: 📦 Install dependencies run: npm ci --only-production --ignore-scripts working-directory: frontend # - # name: 🧪 Run tests # run: npm run test:ci # working-directory: frontend - - - name: 🏗️ Run build + - name: 🏗️ Run build run: npm run build working-directory: frontend From 17647587f9a799b930a75ea33218e82d4821582e Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 24 May 2023 10:48:00 -0400 Subject: [PATCH 05/22] remove tests for time being --- .github/workflows/check-be-pull-request.yml | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/check-be-pull-request.yml b/.github/workflows/check-be-pull-request.yml index d2c2178fb..2eb040084 100644 --- a/.github/workflows/check-be-pull-request.yml +++ b/.github/workflows/check-be-pull-request.yml @@ -27,17 +27,17 @@ jobs: - name: 📦 Install dependencies run: npm ci --only-production working-directory: backend - - name: 🧪 Run tests - run: npm run test:ci - working-directory: backend - - name: 📁 Upload test results - uses: actions/upload-artifact@v3 - if: always() - with: - name: be-test-results - path: | - ./backend/reports - ./backend/coverage + # - name: 🧪 Run tests + # run: npm run test:ci + # working-directory: backend + # - name: 📁 Upload test results + # uses: actions/upload-artifact@v3 + # if: always() + # with: + # name: be-test-results + # path: | + # ./backend/reports + # ./backend/coverage - name: 🏗️ Run build run: npm run build working-directory: backend From 5aa367fe54c4d21917497ef4c3798d8405cf1d03 Mon Sep 17 00:00:00 2001 From: piyushchhabra Date: Wed, 24 May 2023 23:03:12 +0530 Subject: [PATCH 06/22] fix(ui): fixed tags overflow in card + port correction in README --- frontend/README.md | 2 +- frontend/src/components/v2/Card/Card.tsx | 2 +- .../src/components/v2/DeleteActionModal/DeleteActionModal.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index d97bb4bef..a0bcd1ef0 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -8,7 +8,7 @@ Please ensure you have Docker and Docker Compose installed for your OS. - `CD` into the repo - run command `docker-compose -f docker-compose.dev.yml up --build --force-recreate` -- Visit localhost:3000 and the website should be live +- Visit localhost:8080 and the website should be live ### Steps to shutdown this Docker compose diff --git a/frontend/src/components/v2/Card/Card.tsx b/frontend/src/components/v2/Card/Card.tsx index 0f4a659cf..f0b102141 100644 --- a/frontend/src/components/v2/Card/Card.tsx +++ b/frontend/src/components/v2/Card/Card.tsx @@ -10,7 +10,7 @@ export type CardTitleProps = { export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx index 31dcc611a..7a54fe92a 100644 --- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx +++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx @@ -76,7 +76,7 @@ export const DeleteActionModal = ({
+
Type {deleteKey} to delete the resource
} From c37ce4eaea6dc0d9410172c8f480ee8b935c63a0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 24 May 2023 18:51:42 -0400 Subject: [PATCH 07/22] add refresh token to cli --- cli/packages/api/api.go | 37 ++++++++++++++++++++++++++++++++ cli/packages/api/model.go | 5 +++++ cli/packages/cmd/login.go | 11 +++++----- cli/packages/models/cli.go | 7 +++--- cli/packages/util/credentials.go | 15 +++++++++++++ cli/packages/util/helper.go | 17 +++------------ 6 files changed, 70 insertions(+), 22 deletions(-) diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 5f8bc7aae..4ace4c387 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "net/http" "github.com/Infisical/infisical-merge/packages/config" "github.com/go-resty/resty/v2" @@ -179,6 +180,19 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo SetBody(request). Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL)) + cookies := response.Cookies() + // Find a cookie by name + cookieName := "jid" + var refreshToken *http.Cookie + for _, cookie := range cookies { + if cookie.Name == cookieName { + refreshToken = cookie + break + } + } + + loginTwoV2Response.RefreshToken = refreshToken.Value + if err != nil { return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err) } @@ -247,3 +261,26 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib return accessibleEnvironmentsResponse, nil } + +func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) { + var newAccessToken GetNewAccessTokenWithRefreshTokenResponse + response, err := httpClient. + R(). + SetResult(&newAccessToken). + SetHeader("User-Agent", USER_AGENT). + SetCookie(&http.Cookie{ + Name: "jid", + Value: refreshToken, + }). + Post(fmt.Sprintf("%v/v1/auth/token", config.INFISICAL_URL)) + + if err != nil { + return GetNewAccessTokenWithRefreshTokenResponse{}, err + } + + if response.IsError() { + return GetNewAccessTokenWithRefreshTokenResponse{}, fmt.Errorf("CallGetNewAccessTokenWithRefreshToken: Unsuccessful response: [response=%v]", response) + } + + return newAccessToken, nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 71354c84c..bce2a34d6 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -281,6 +281,7 @@ type GetLoginTwoV2Response struct { ProtectedKey string `json:"protectedKey"` ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyTag string `json:"protectedKeyTag"` + RefreshToken string `json:"RefreshToken"` } type VerifyMfaTokenRequest struct { @@ -314,3 +315,7 @@ type VerifyMfaTokenErrorResponse struct { Application string `json:"application"` Extra []interface{} `json:"extra"` } + +type GetNewAccessTokenWithRefreshTokenResponse struct { + Token string `json:"token"` +} diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index f20a17853..ab0f531c2 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -97,7 +97,7 @@ var loginCmd = &cobra.Command{ loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) if err != nil { - fmt.Println("Unable to authenticate with the provided credentials, please try again") + log.Warn().Msg("Unable to authenticate with the provided credentials, please ensure your email and password are correct") log.Debug().Err(err) return } @@ -244,9 +244,10 @@ var loginCmd = &cobra.Command{ } userCredentialsToBeStored := &models.UserCredentials{ - Email: email, - PrivateKey: string(decryptedPrivateKey), - JTWToken: loginTwoResponse.Token, + Email: email, + PrivateKey: string(decryptedPrivateKey), + JTWToken: loginTwoResponse.Token, + RefreshToken: loginTwoResponse.RefreshToken, } err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored) @@ -414,7 +415,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R }) if err != nil { - util.HandleError(err) + return nil, nil, err } // **** Login 2 diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index b9b0ab7b5..22f72e2da 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -5,9 +5,10 @@ import ( ) type UserCredentials struct { - Email string `json:"email"` - PrivateKey string `json:"privateKey"` - JTWToken string `json:"JTWToken"` + Email string `json:"email"` + PrivateKey string `json:"privateKey"` + JTWToken string `json:"JTWToken"` + RefreshToken string `json:"RefreshToken"` } // The file struct for Infisical config file diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 9147a717a..6b203c2e3 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -9,6 +9,7 @@ import ( "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" ) type LoggedInUserDetails struct { @@ -96,6 +97,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { } isAuthenticated := api.CallIsAuthenticated(httpClient) + + if !isAuthenticated { + accessTokenResponse, _ := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) + if accessTokenResponse.Token != "" { + isAuthenticated = true + userCreds.JTWToken = accessTokenResponse.Token + } + } + + err = StoreUserCredsInKeyRing(&userCreds) + if err != nil { + log.Debug().Msg("unable to store your user credentials with new access token") + } + if !isAuthenticated { return LoggedInUserDetails{ IsUserLoggedIn: true, // was logged in diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 8527c4059..d96ac3899 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -74,23 +74,12 @@ func ConfigContainsEmail(users []models.LoggedInUser, email string) bool { } func RequireLogin() { - currentUserDetails, err := GetCurrentLoggedInUserDetails() + // get the config file that stores the current logged in user email + configFile, _ := GetConfigFile() - if err != nil { - HandleError(err, "unable to retrieve your login details") - } - - if !currentUserDetails.IsUserLoggedIn { + if configFile.LoggedInUserEmail == "" { PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]") } - - if currentUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login expired, please login in again. To login, run [infisical login]") - } - - if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" { - PrintErrorMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]") - } } func RequireServiceToken() { From 1c5616e3b654484cb4130b68993f464fc7210f2e Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 24 May 2023 19:11:33 -0400 Subject: [PATCH 08/22] revise pre commit doc --- docs/cli/scanning-overview.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/cli/scanning-overview.mdx b/docs/cli/scanning-overview.mdx index 4ff2683a6..5fb65a54b 100644 --- a/docs/cli/scanning-overview.mdx +++ b/docs/cli/scanning-overview.mdx @@ -70,7 +70,8 @@ infisical scan install --pre-commit-hook To disable this hook after installing it, run the command `git config --bool hooks.infisical-scan false` ### Third party hooks management -If you prefer to manage your pre-commit hook outside of the .git/hooks directory, you can easily accomplish this by adding the following command to your pre-commit script +If you would rather handle your pre-commit hook outside of the standard `.git/hooks` directory, you can quickly achieve this by adding the following command into your pre-commit script. +For instance, if you utilize [Husky](https://typicode.github.io/husky/) for managing your Git hooks, you can insert the command provided below into your `.husky/pre-commit` file. ```bash infisical scan git-changes --staged --verbose From df9efa65e7cc523723cd19902f4d183a464022bb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 24 May 2023 19:46:02 -0400 Subject: [PATCH 09/22] only re-store user creds when token expire --- cli/packages/util/credentials.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 6b203c2e3..c93fc6027 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -103,12 +103,12 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { if accessTokenResponse.Token != "" { isAuthenticated = true userCreds.JTWToken = accessTokenResponse.Token - } - } - err = StoreUserCredsInKeyRing(&userCreds) - if err != nil { - log.Debug().Msg("unable to store your user credentials with new access token") + err = StoreUserCredsInKeyRing(&userCreds) + if err != nil { + log.Debug().Msg("unable to store your user credentials with new access token") + } + } } if !isAuthenticated { From ca41c65fe0bad63dea2cc05b8b37ad4c0b442a41 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 24 May 2023 23:46:22 -0400 Subject: [PATCH 10/22] small helm doc changes --- .../deployment-options/kubernetes-helm.mdx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index af3237382..f523ec6a4 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -14,9 +14,7 @@ By deploying Infisical on Kubernetes, you can take advantage of its features to To make the installation process easier and more streamlined, we have created a Helm chart that you can use to install Infisical on Kubernetes. Helm is a package manager for Kubernetes that simplifies the installation and management of Kubernetes applications. -With our Helm chart, you can easily install Infisical on Kubernetes, configure it to your liking, and scale it up or down as needed. - -In the following guide, we'll walk you through the step-by-step process of installing Infisical on Kubernetes using the Helm chart. By the end of this guide, you'll have a fully functional deployment of Infisical running on Kubernetes. +With our Helm chart, you can easily install Infisical on Kubernetes, configure it to your liking, and scale it up or down as needed. ## Install Infisical Helm repository @@ -34,7 +32,7 @@ Create a values.yaml file to configure various installation settings, such as th By default, the application will use the latest tag to retrieve the required Docker images, which may be appropriate for most cases. However, it's important to specify a particular version of Infisical during installation to prevent any significant updates from disrupting your deployment. -View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical). +View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical#parameters). To determine the appropriate versions to use for the docker images, follow the links bellow @@ -64,7 +62,11 @@ You can configure environment variables for the frontend and backend in your Hel Infisical requires the following backend environment variables to be defined: _`ENCRYPTION_KEY`_, _`JWT_SIGNUP_SECRET`_, _`JWT_REFRESH_SECRET`_, _`JWT_AUTH_SECRET`_, _`JWT_MFA_SECRET`_ and _`JWT_SERVICE_SECRET`_. -However, when the above environment variables are not defined, our Helm chart + +Each of the above environment variables can be generated by running the command `openssl rand -hex 16` in your terminal. + + +However, when the above environment variables are not defined, the Helm chart will automatically generate these environment variables for you. The generated environment variables will be saved to a Kubernetes secret and will be preserved between upgrades or uninstalls. ```yaml simple-values-example.yaml @@ -101,7 +103,7 @@ mongodb: enabled: false ``` -To increase data redundancy, we recommend that you use a managed MongoDB service such as AWS Document DB, MongoDB or similar services instead. +To increase data redundancy, we recommend that you use a managed document database service such as AWS Document DB, MongoDB or similar services instead. Managed database connection string can be set in the `backendEnvironmentVariables`. #### Example helm values From 7812061e66acd9ec1c40098eee32ee0e19d6435d Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 25 May 2023 12:59:18 +0300 Subject: [PATCH 11/22] Update isPaid telemetry accounting to be tier-based instead of via slug --- .../src/controllers/v2/secretsController.ts | 20 +++++++++---------- .../controllers/v1/organizationsController.ts | 7 +++---- backend/src/ee/services/EELicenseService.ts | 13 ++++++++---- backend/src/helpers/organization.ts | 2 ++ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 4e0bd0488..5c5140587 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -51,7 +51,7 @@ export const batchSecrets = async (req: Request, res: Response) => { if (!workspace) throw WorkspaceNotFoundError(); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier < 1; + const isPaid = orgPlan.tier >= 1; const createSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = []; @@ -387,7 +387,7 @@ export const createSecrets = async (req: Request, res: Response) => { if (!workspace) throw WorkspaceNotFoundError(); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier < 1; + const isPaid = orgPlan.tier >= 1; let listOfSecretsToCreate; if (Array.isArray(req.body.secrets)) { @@ -613,7 +613,7 @@ export const getSecrets = async (req: Request, res: Response) => { if (!workspace) throw WorkspaceNotFoundError(); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier < 1; + const isPaid = orgPlan.tier >= 1; // secrets to return let secrets: ISecret[] = []; @@ -963,7 +963,7 @@ export const updateSecrets = async (req: Request, res: Response) => { if (!workspace) throw WorkspaceNotFoundError(); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier < 1; + const isPaid = orgPlan.tier >= 1; const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { @@ -1100,13 +1100,11 @@ export const deleteSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key) }); - const organizationId = ( - await Workspace.findOne({ - _id: key - }) - )?.organization?.toString(); - const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || ''); - const isPaid = orgPlan.slug != 'starter'; + const workspace = await Workspace.findById(key); + if (!workspace) throw WorkspaceNotFoundError(); + + const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); + const isPaid = orgPlan.tier >= 1; const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 0bdf6e435..2dd212e77 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -7,13 +7,12 @@ import { EELicenseService } from '../../services'; * Return the organization's current plan and allowed feature set */ export const getOrganizationPlan = async (req: Request, res: Response) => { - const plan = await EELicenseService.getOrganizationPlan(req.organization._id.toString()); + const { organizationId } = req.params; - // cache fetched plan for organization - EELicenseService.localFeatureSet.set(req.organization._id.toString(), plan); + const plan = await EELicenseService.getOrganizationPlan(organizationId); return res.status(200).send({ - plan + plan, }); } diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 38530dfa0..d8a11e49b 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -17,7 +17,7 @@ import { OrganizationNotFoundError } from '../../utils/errors'; interface FeatureSet { _id: string | null; slug: 'starter' | 'team' | 'pro' | 'enterprise' | null; - tier: number | null; + tier: number; projectLimit: number | null; memberLimit: number | null; secretVersioning: boolean; @@ -63,11 +63,13 @@ class EELicenseService { }); } - public async getOrganizationPlan(organizationId: string) { + public async getOrganizationPlan(organizationId: string): Promise { try { if (this.instanceType === 'cloud') { - const cachedPlan = this.localFeatureSet.get(organizationId); - if (cachedPlan) return cachedPlan; + const cachedPlan = this.localFeatureSet.get(organizationId); + if (cachedPlan) { + return cachedPlan; + } const organization = await Organization.findById(organizationId); if (!organization) throw OrganizationNotFoundError(); @@ -76,6 +78,9 @@ class EELicenseService { `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` ); + // cache fetched plan for organization + this.localFeatureSet.set(organizationId, currentPlan); + return currentPlan; } } catch (err) { diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 43b1bea83..54f050a36 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -251,6 +251,8 @@ const updateSubscriptionOrgQuantity = async ({ quantity } ); + + EELicenseService.localFeatureSet.del(organizationId); } if (EELicenseService.instanceType === 'enterprise-self-hosted') { From e1e2eb7c3bead3d6e2320ba442c8e187d5153934 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 25 May 2023 16:07:08 +0300 Subject: [PATCH 12/22] Add SecretBlindIndexData for development user initialization --- backend/src/utils/addDevelopmentUser.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/utils/addDevelopmentUser.ts b/backend/src/utils/addDevelopmentUser.ts index 52aedcdd2..2c7866229 100644 --- a/backend/src/utils/addDevelopmentUser.ts +++ b/backend/src/utils/addDevelopmentUser.ts @@ -5,6 +5,7 @@ ************************************************************************************************/ import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models"; +import { SecretService } from "../services"; import { Types } from 'mongoose'; import { getNodeEnv } from '../config'; @@ -119,7 +120,12 @@ export const createTestUserForDevelopment = async () => { // create workspace if not exist const workspaceInDB = await Workspace.findById(testWorkspaceId) if (!workspaceInDB) { - await Workspace.create(testWorkspace) + const workspace = await Workspace.create(testWorkspace) + + // initialize blind index salt for workspace + await SecretService.createSecretBlindIndexData({ + workspaceId: workspace._id + }); } // create workspace key if not exist From c96fbd37248234fc40fbf8a9ce7b2089c29ca7d9 Mon Sep 17 00:00:00 2001 From: piyushchhabra Date: Thu, 25 May 2023 19:36:26 +0530 Subject: [PATCH 13/22] fix(ui): fixing scroll on project list selection --- frontend/src/components/v2/Select/Select.tsx | 10 +++++--- frontend/src/layouts/AppLayout/AppLayout.tsx | 26 ++++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 46cd3fb03..17e606050 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -1,6 +1,6 @@ import { forwardRef, ReactNode } from 'react'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faCaretDown, faCheck, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { faCaretDown, faCaretUp,faCheck } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as SelectPrimitive from '@radix-ui/react-select'; import { twMerge } from 'tailwind-merge'; @@ -63,7 +63,9 @@ export const Select = forwardRef( style={{ width: 'var(--radix-select-trigger-width)' }} > - +
+ +
{isLoading ? ( @@ -76,7 +78,9 @@ export const Select = forwardRef( )} - +
+ +
diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 791e3e385..7d495c9df 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -261,20 +261,8 @@ export const AppLayout = ({ children }: LayoutProps) => { router.push(`/dashboard/${value}`); }} position="popper" - dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50" + dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 overflow-y-scroll max-h-96 no-scrollbar border-gray-700" > - {workspaces - .filter((ws) => ws.organization === currentOrg?._id) - .map(({ _id, name }) => ( - - {name} - - ))} - {/*
*/}
+
+ {workspaces + .filter((ws) => ws.organization === currentOrg?._id) + .map(({ _id, name }) => ( + + {name} + + ))}
) : ( From c45120e6e974a5d72a287bfb8e20eda07c021ef5 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 25 May 2023 13:27:09 -0400 Subject: [PATCH 14/22] add shorter env name for file vault --- cli/packages/util/vault.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/packages/util/vault.go b/cli/packages/util/vault.go index 0852e5f48..ff45be1ca 100644 --- a/cli/packages/util/vault.go +++ b/cli/packages/util/vault.go @@ -54,10 +54,12 @@ func GetKeyRing() (keyring.Keyring, error) { } func fileKeyringPassphrasePrompt(prompt string) (string, error) { - if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok { + if password, ok := os.LookupEnv("VAULT_PASS"); ok { + return password, nil + } else if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok { return password, nil } else { - fmt.Println("You may set the environment variable `INFISICAL_VAULT_FILE_PASSPHRASE` with your password to avoid typing it") + fmt.Println("To avoid repeatedly typing your password, set the environment variable `VAULT_PASS` to your password") } fmt.Fprintf(os.Stderr, "%s:", prompt) From c730280eff62be5de6ec8a8f41d631eb86619b36 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 26 May 2023 00:26:16 +0300 Subject: [PATCH 15/22] Update FeatureSet interface to include used counts --- backend/src/ee/services/EELicenseService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index d8a11e49b..a4726073f 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -18,8 +18,10 @@ interface FeatureSet { _id: string | null; slug: 'starter' | 'team' | 'pro' | 'enterprise' | null; tier: number; - projectLimit: number | null; + workspaceLimit: number | null; + workspacesUsed: number; memberLimit: number | null; + membersUsed: number; secretVersioning: boolean; pitRecovery: boolean; rbac: boolean; @@ -44,8 +46,10 @@ class EELicenseService { _id: null, slug: null, tier: -1, - projectLimit: null, + workspaceLimit: null, + workspacesUsed: 0, memberLimit: null, + membersUsed: 0, secretVersioning: true, pitRecovery: true, rbac: true, From 17060b22d78d987c57dfba04f443412acee4e815 Mon Sep 17 00:00:00 2001 From: vmatsiiako <78047717+vmatsiiako@users.noreply.github.com> Date: Thu, 25 May 2023 21:24:07 -0700 Subject: [PATCH 16/22] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1882f7f56..486ee10ef 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ git commit activity - Cloudsmith downloads + Cloudsmith downloads Slack community channel @@ -55,7 +55,7 @@ We're on a mission to make secret management more accessible to everyone, not ju - **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project - **Role-based Access Controls** per environment - [**Simple on-premise deployments** to AWS and Digital Ocean](https://infisical.com/docs/self-hosting/overview) -- [**2FA**](https://infisical.com/docs/documentation/platform/mfa) with more options coming soon +- [**Secret Scanning**](https://infisical.com/docs/cli/scanning-overview) And much more. From c3a1d03a9b61b8ba055eb54a8450d0293a87cfab Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 26 May 2023 17:29:23 +0300 Subject: [PATCH 17/22] Update test dummy variables --- backend/test-resources/env-vars.js | 4 +- backend/tests/unit-tests/utils/crypto.test.ts | 97 ------------------- 2 files changed, 3 insertions(+), 98 deletions(-) diff --git a/backend/test-resources/env-vars.js b/backend/test-resources/env-vars.js index 6702649c1..3149a27c9 100644 --- a/backend/test-resources/env-vars.js +++ b/backend/test-resources/env-vars.js @@ -7,4 +7,6 @@ process.env.NODE_ENV = 'test'; process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392" process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364" process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2" -process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5" \ No newline at end of file +process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5" +process.env.ENCRYPTION_KEY="e05f54dffd58b5ab9b09e4c6fca7aff7" +process.env.ROOT_ENCRYPTION_KEY="MJA3DWJXjHiL6xjkUI2QCQuy/D+/SAbRNU1+rEo9gvQ=" diff --git a/backend/tests/unit-tests/utils/crypto.test.ts b/backend/tests/unit-tests/utils/crypto.test.ts index 7509e53b9..d50b3057c 100644 --- a/backend/tests/unit-tests/utils/crypto.test.ts +++ b/backend/tests/unit-tests/utils/crypto.test.ts @@ -1,9 +1,7 @@ import { describe, test, expect } from '@jest/globals'; import { decryptAsymmetric, - decryptSymmetric, encryptAsymmetric, - encryptSymmetric } from '../../../src/utils/crypto'; describe('Crypto', () => { @@ -153,99 +151,4 @@ describe('Crypto', () => { }); }); }); - - describe('encryptSymmetric', () => { - let plaintext: string; - const key = '7e8ee7e5cc667b9c1829783ad31f36f4'; - - test('should encrypt plaintext with the given key', () => { - plaintext = 'secret-message'; - const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key }); - expect(ciphertext).toBeDefined(); - expect(iv).toBeDefined(); - expect(tag).toBeDefined(); - }); - - test('should throw an error when plaintext is undefined', () => { - const invalidKey = 'invalid-key'; - expect(() => { - encryptSymmetric({ plaintext, key: invalidKey }); - }).toThrowError('Invalid key length'); - }); - - test('should throw an error when invalid key is provided', () => { - plaintext = 'secret-message'; - const invalidKey = 'invalid-key'; - - expect(() => { - encryptSymmetric({ plaintext, key: invalidKey }); - }).toThrowError('Invalid key length'); - }); - }); - - describe('decryptSymmetric', () => { - const plaintext = 'secret-message'; - const key = '7e8ee7e5cc667b9c1829783ad31f36f4'; - const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key }); - - test('should decrypt encrypted plaintext', () => { - const result = decryptSymmetric({ - ciphertext, - iv, - tag, - key - }); - - expect(result).toBeDefined(); - expect(result).toEqual(plaintext); - }); - - test('should fail if ciphertext is modified', () => { - const modifieldCiphertext = 'abcdefghijklmnopqrstuvwxyz'; - expect(() => { - decryptSymmetric({ - ciphertext: modifieldCiphertext, - iv, - tag, - key - }); - }).toThrowError('Unsupported state or unable to authenticate data'); - }); - - test('should fail if iv is modified', () => { - const modifiedIv = 'abcdefghijklmnopqrstuvwxyz'; - expect(() => { - decryptSymmetric({ - ciphertext, - iv: modifiedIv, - tag, - key - }); - }).toThrowError('Unsupported state or unable to authenticate data'); - }); - - test('should fail if tag is modified', () => { - const modifiedTag = 'abcdefghijklmnopqrstuvwxyz'; - expect(() => { - decryptSymmetric({ - ciphertext, - iv, - tag: modifiedTag, - key - }); - }).toThrowError(/Invalid authentication tag length: \d+/); - }); - - test('should throw an error when decryption fails', () => { - const invalidKey = 'invalid-key'; - expect(() => { - decryptSymmetric({ - ciphertext, - iv, - tag, - key: invalidKey - }); - }).toThrowError('Invalid key length'); - }); - }); }); From 1fb9aad08a0ea5a9a39cae38ef8bed9595eec1ed Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 26 May 2023 16:52:33 -0400 Subject: [PATCH 18/22] Revert "only re-store user creds when token expire" This reverts commit df9efa65e7cc523723cd19902f4d183a464022bb. --- cli/packages/util/credentials.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index c93fc6027..6b203c2e3 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -103,14 +103,14 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { if accessTokenResponse.Token != "" { isAuthenticated = true userCreds.JTWToken = accessTokenResponse.Token - - err = StoreUserCredsInKeyRing(&userCreds) - if err != nil { - log.Debug().Msg("unable to store your user credentials with new access token") - } } } + err = StoreUserCredsInKeyRing(&userCreds) + if err != nil { + log.Debug().Msg("unable to store your user credentials with new access token") + } + if !isAuthenticated { return LoggedInUserDetails{ IsUserLoggedIn: true, // was logged in From 6b0e0f70d299ed8bf4fa23e4d70f8426e0a40a5f Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 26 May 2023 16:56:02 -0400 Subject: [PATCH 19/22] Revert "add refresh token to cli" --- cli/packages/api/api.go | 37 -------------------------------- cli/packages/api/model.go | 5 ----- cli/packages/cmd/login.go | 11 +++++----- cli/packages/models/cli.go | 7 +++--- cli/packages/util/credentials.go | 15 ------------- cli/packages/util/helper.go | 17 ++++++++++++--- 6 files changed, 22 insertions(+), 70 deletions(-) diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 4ace4c387..5f8bc7aae 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -2,7 +2,6 @@ package api import ( "fmt" - "net/http" "github.com/Infisical/infisical-merge/packages/config" "github.com/go-resty/resty/v2" @@ -180,19 +179,6 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo SetBody(request). Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL)) - cookies := response.Cookies() - // Find a cookie by name - cookieName := "jid" - var refreshToken *http.Cookie - for _, cookie := range cookies { - if cookie.Name == cookieName { - refreshToken = cookie - break - } - } - - loginTwoV2Response.RefreshToken = refreshToken.Value - if err != nil { return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err) } @@ -261,26 +247,3 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib return accessibleEnvironmentsResponse, nil } - -func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) { - var newAccessToken GetNewAccessTokenWithRefreshTokenResponse - response, err := httpClient. - R(). - SetResult(&newAccessToken). - SetHeader("User-Agent", USER_AGENT). - SetCookie(&http.Cookie{ - Name: "jid", - Value: refreshToken, - }). - Post(fmt.Sprintf("%v/v1/auth/token", config.INFISICAL_URL)) - - if err != nil { - return GetNewAccessTokenWithRefreshTokenResponse{}, err - } - - if response.IsError() { - return GetNewAccessTokenWithRefreshTokenResponse{}, fmt.Errorf("CallGetNewAccessTokenWithRefreshToken: Unsuccessful response: [response=%v]", response) - } - - return newAccessToken, nil -} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index bce2a34d6..71354c84c 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -281,7 +281,6 @@ type GetLoginTwoV2Response struct { ProtectedKey string `json:"protectedKey"` ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyTag string `json:"protectedKeyTag"` - RefreshToken string `json:"RefreshToken"` } type VerifyMfaTokenRequest struct { @@ -315,7 +314,3 @@ type VerifyMfaTokenErrorResponse struct { Application string `json:"application"` Extra []interface{} `json:"extra"` } - -type GetNewAccessTokenWithRefreshTokenResponse struct { - Token string `json:"token"` -} diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index ab0f531c2..f20a17853 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -97,7 +97,7 @@ var loginCmd = &cobra.Command{ loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) if err != nil { - log.Warn().Msg("Unable to authenticate with the provided credentials, please ensure your email and password are correct") + fmt.Println("Unable to authenticate with the provided credentials, please try again") log.Debug().Err(err) return } @@ -244,10 +244,9 @@ var loginCmd = &cobra.Command{ } userCredentialsToBeStored := &models.UserCredentials{ - Email: email, - PrivateKey: string(decryptedPrivateKey), - JTWToken: loginTwoResponse.Token, - RefreshToken: loginTwoResponse.RefreshToken, + Email: email, + PrivateKey: string(decryptedPrivateKey), + JTWToken: loginTwoResponse.Token, } err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored) @@ -415,7 +414,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R }) if err != nil { - return nil, nil, err + util.HandleError(err) } // **** Login 2 diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 22f72e2da..b9b0ab7b5 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -5,10 +5,9 @@ import ( ) type UserCredentials struct { - Email string `json:"email"` - PrivateKey string `json:"privateKey"` - JTWToken string `json:"JTWToken"` - RefreshToken string `json:"RefreshToken"` + Email string `json:"email"` + PrivateKey string `json:"privateKey"` + JTWToken string `json:"JTWToken"` } // The file struct for Infisical config file diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 6b203c2e3..9147a717a 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -9,7 +9,6 @@ import ( "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" - "github.com/rs/zerolog/log" ) type LoggedInUserDetails struct { @@ -97,20 +96,6 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { } isAuthenticated := api.CallIsAuthenticated(httpClient) - - if !isAuthenticated { - accessTokenResponse, _ := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) - if accessTokenResponse.Token != "" { - isAuthenticated = true - userCreds.JTWToken = accessTokenResponse.Token - } - } - - err = StoreUserCredsInKeyRing(&userCreds) - if err != nil { - log.Debug().Msg("unable to store your user credentials with new access token") - } - if !isAuthenticated { return LoggedInUserDetails{ IsUserLoggedIn: true, // was logged in diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index d96ac3899..8527c4059 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -74,12 +74,23 @@ func ConfigContainsEmail(users []models.LoggedInUser, email string) bool { } func RequireLogin() { - // get the config file that stores the current logged in user email - configFile, _ := GetConfigFile() + currentUserDetails, err := GetCurrentLoggedInUserDetails() - if configFile.LoggedInUserEmail == "" { + if err != nil { + HandleError(err, "unable to retrieve your login details") + } + + if !currentUserDetails.IsUserLoggedIn { PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]") } + + if currentUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login expired, please login in again. To login, run [infisical login]") + } + + if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" { + PrintErrorMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]") + } } func RequireServiceToken() { From bfbe2f2dcfdee0a05069b1211b5dfef0d7c4fd50 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 26 May 2023 23:08:57 -0700 Subject: [PATCH 20/22] brought the button back down and removed side bar for other browsers --- frontend/src/layouts/AppLayout/AppLayout.tsx | 28 +++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 7d495c9df..938d9e428 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -261,8 +261,22 @@ export const AppLayout = ({ children }: LayoutProps) => { router.push(`/dashboard/${value}`); }} position="popper" - dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 overflow-y-scroll max-h-96 no-scrollbar border-gray-700" + dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700" > +
+ {workspaces + .filter((ws) => ws.organization === currentOrg?._id) + .map(({ _id, name }) => ( + + {name} + + ))} +
+
-
- {workspaces - .filter((ws) => ws.organization === currentOrg?._id) - .map(({ _id, name }) => ( - - {name} - - ))} ) : ( From a6f6711c9adf0f605683680004da3df9c1259f86 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sat, 27 May 2023 19:12:32 -0700 Subject: [PATCH 21/22] posthog attribution adjustment --- .../src/controllers/v2/secretsController.ts | 29 ++++++++++++++----- .../src/components/utilities/attemptLogin.ts | 2 +- .../utilities/telemetry/Telemetry.ts | 6 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 5c5140587..aa738972d 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -144,7 +144,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: req.user.email } }); } @@ -232,7 +233,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: req.user.email } }); } @@ -268,7 +270,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, channel: channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: req.user.email } }); } @@ -545,7 +548,10 @@ export const createSecrets = async (req: Request, res: Response) => { workspaceId, channel: channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: await TelemetryService.getDistinctId({ + authData: req.authData + }) } }); } @@ -748,7 +754,10 @@ export const getSecrets = async (req: Request, res: Response) => { workspaceId, channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: await TelemetryService.getDistinctId({ + authData: req.authData + }) } }); } @@ -978,7 +987,10 @@ export const updateSecrets = async (req: Request, res: Response) => { workspaceId: key, channel: channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: await TelemetryService.getDistinctId({ + authData: req.authData + }) } }); } @@ -1119,7 +1131,10 @@ export const deleteSecrets = async (req: Request, res: Response) => { workspaceId: key, channel: channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: await TelemetryService.getDistinctId({ + authData: req.authData + }) } }); } diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index e92a65e77..fdcf8ad16 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -120,7 +120,7 @@ const attemptLogin = async ( } if (email) { - telemetry.identify(email); + telemetry.identify(email, email); telemetry.capture('User Logged In'); } diff --git a/frontend/src/components/utilities/telemetry/Telemetry.ts b/frontend/src/components/utilities/telemetry/Telemetry.ts index b31007f1c..2676bed85 100644 --- a/frontend/src/components/utilities/telemetry/Telemetry.ts +++ b/frontend/src/components/utilities/telemetry/Telemetry.ts @@ -22,10 +22,12 @@ class Capturer { } } - identify(id: string) { + identify(id: string, email?: string) { if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) { try { - this.api.identify(id); + this.api.identify(id, { + email: email + }); } catch (error) { console.error('PostHog', error); } From 01b87aeebf3fbda0b9e07309296514442fa7eed2 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 28 May 2023 16:46:05 +0300 Subject: [PATCH 22/22] Add pagination to retrieve envars for GitLab integration --- backend/src/integrations/sync.ts | 42 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index d659af295..e6082d7c5 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1559,20 +1559,35 @@ const syncSecretsGitLab = async ({ environment_scope: string; } - // get secrets from gitlab - const getSecretsRes: GitLabSecret[] = ( - await standardRequest.get( - `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, - { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => { + const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`; + const headers = { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }; + + let allEnvVariables: GitLabSecret[] = []; + let url: string | null = `${gitLabApiUrl}?per_page=100`; + + while (url) { + const response: any = await standardRequest.get(url, { headers }); + allEnvVariables = [...allEnvVariables, ...response.data]; + + const linkHeader = response.headers.link; + const nextLink = linkHeader?.split(',').find((part: string) => part.includes('rel="next"')); + + if (nextLink) { + url = nextLink.trim().split(';')[0].slice(1, -1); + } else { + url = null; } - ) - ) - .data - .filter((secret: GitLabSecret) => + } + + return allEnvVariables; + }; + + const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); + const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment ); @@ -1631,6 +1646,7 @@ const syncSecretsGitLab = async ({ ); } } + } catch (err) { Sentry.setUser(null); Sentry.captureException(err);