diff --git a/.github/workflows/check-be-pull-request.yml b/.github/workflows/check-be-pull-request.yml index 3cb010e8c..2eb040084 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 @@ -26,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 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 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. diff --git a/backend/package-lock.json b/backend/package-lock.json index 1662929e7..063999c60 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", @@ -6808,13 +6808,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" } @@ -18432,13 +18431,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" } diff --git a/backend/package.json b/backend/package.json index bb4be61c1..b8f91e6c9 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 394154fea..4613a05c9 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,12 +1,19 @@ import InfisicalClient from 'infisical-node'; -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 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 getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true' -export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).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; @@ -50,8 +57,14 @@ export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWOR export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue; export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical'; -export const getLicenseKey = async () => (await client.getSecret('LICENSE_KEY')).secretValue; -export const getLicenseServerKey = async () => (await client.getSecret('LICENSE_SERVER_KEY')).secretValue; +export const getLicenseKey = async () => { + const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue; + return secretValue === '' ? undefined : secretValue; +} +export const getLicenseServerKey = async () => { + const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue; + return secretValue === '' ? undefined : secretValue; +} export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com'; // TODO: deprecate from here diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 0d5947a4f..97032aaa4 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 e5af676b3..681b65459 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -39,7 +39,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/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/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 4e0bd0488..4a5618454 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -7,7 +7,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, WorkspaceNotFoundError } from '../../utils/errors'; import { EventService } from '../../services'; @@ -51,7 +53,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[] = []; @@ -85,7 +87,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': @@ -100,6 +104,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': @@ -144,7 +150,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: req.user.email } }); } @@ -201,6 +208,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 })); @@ -232,7 +241,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: req.user.email } }); } @@ -268,7 +278,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, channel: channel, userAgent: req.headers?.['user-agent'], - isPaid + isPaid, + email: req.user.email } }); } @@ -387,7 +398,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)) { @@ -457,6 +468,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, tags }); }) @@ -503,7 +516,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag + secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 })) }); @@ -545,7 +560,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 + }) } }); } @@ -613,7 +631,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[] = []; @@ -748,7 +766,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 + }) } }); } @@ -852,6 +873,8 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, tags, ...(( secretCommentCiphertext !== undefined && @@ -905,6 +928,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 }); }) @@ -963,7 +988,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) { @@ -978,7 +1003,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 + }) } }); } @@ -1100,13 +1128,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) { @@ -1121,7 +1147,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/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/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/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/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/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 38530dfa0..a4726073f 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -17,9 +17,11 @@ import { OrganizationNotFoundError } from '../../utils/errors'; interface FeatureSet { _id: string | null; slug: 'starter' | 'team' | 'pro' | 'enterprise' | null; - tier: number | null; - projectLimit: number | null; + tier: number; + 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, @@ -63,11 +67,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 +82,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/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/auth.ts b/backend/src/helpers/auth.ts index 55f4542d0..177df8ad8 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -42,7 +42,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 96ad452d9..04dfd7e36 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -4,107 +4,26 @@ import { BotKey, Secret, ISecret, - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData, + IUser } from "../models"; import { generateKeyPair, - encryptSymmetric, - decryptSymmetric, - decryptAsymmetric, -} from "../utils/crypto"; + encryptSymmetric128BitHexKeyUTF8, + decryptSymmetric128BitHexKeyUTF8, + decryptAsymmetric +} 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 { validateMembership } from "../helpers/membership"; -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] @@ -119,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 } = encryptSymmetric({ - 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; }; /** @@ -161,14 +109,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, @@ -189,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 = decryptSymmetric({ - 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" + }); }; /** @@ -234,7 +202,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 +234,7 @@ const decryptSymmetricHelper = async ({ tag: string; }) => { const key = await getKey({ workspaceId: workspaceId.toString() }); - const plaintext = decryptSymmetric({ + const plaintext = decryptSymmetric128BitHexKeyUTF8({ ciphertext, iv, tag, @@ -277,9 +245,8 @@ const decryptSymmetricHelper = async ({ }; export { - validateClientForBot, 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/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 43b1bea83..74c5df71d 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,12 +10,6 @@ import { getStripeProductTeam, getStripeProductStarter, } from "../config"; -import { - UnauthorizedRequestError, - OrganizationNotFoundError, -} from "../utils/errors"; -import { validateUserClientForOrganization } from "../helpers/user"; -import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount"; import { EELicenseService } from '../ee/services'; @@ -40,88 +21,6 @@ import { licenseKeyRequest } from '../config/request'; -/** - * 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] * @param {Object} obj @@ -251,6 +150,8 @@ const updateSubscriptionOrgQuantity = async ({ quantity } ); + + EELicenseService.localFeatureSet.del(organizationId); } if (EELicenseService.instanceType === 'enterprise-self-hosted') { @@ -273,8 +174,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 c184bad9c..562c173fa 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -7,58 +7,35 @@ 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'; -import { - encryptSymmetric, - decryptSymmetric +import { + encryptSymmetric128BitHexKeyUTF8, + decryptSymmetric128BitHexKeyUTF8 } from '../utils/crypto'; -import { getEncryptionKey } from '../config'; +import { getEncryptionKey, client, getRootEncryptionKey } from '../config'; import { TelemetryService } from '../services'; import { EESecretService, @@ -69,199 +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' - }); -} - -/** - * 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] @@ -273,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 - } = encryptSymmetric({ - 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(); + } } /** @@ -306,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 = decryptSymmetric({ - 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; } /** @@ -376,7 +195,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, @@ -464,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({ @@ -481,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 @@ -771,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 @@ -932,9 +757,6 @@ const deleteSecretHelper = async ({ } export { - validateClientForSecret, - validateClientForSecrets, - initSecretBlindIndexDataHelper, 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 2c6a50e21..6bc880981 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,136 +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 { encryptSymmetric } from '../utils/crypto'; 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, workspace }); - } - - 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, workspace }); - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for workspace' - }); -} - /** * Create a workspace with name [name] in organization with id [organizationId] * and a bot for it. @@ -203,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 36e2022e0..b77da9fb2 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,34 +1,26 @@ import dotenv from "dotenv"; 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 { EELicenseService } from "./ee/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"; +import express from 'express'; +import helmet from 'helmet'; +import cors from 'cors'; +import { DatabaseService } from './services'; +import { EELicenseService } from './ee/services'; +import { setUpHealthEndpoint } from './services/health'; +import cookieParser from 'cookie-parser'; +import swaggerUi = require('swagger-ui-express'); // 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"); +const swaggerFile = require('../spec.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const swaggerFile = require("../spec.json"); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const requestIp = require("request-ip"); -import { apiLimiter } from "./helpers/rateLimiter"; +const requestIp = require('request-ip'); +import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, secret as eeSecretRouter, secretSnapshot as eeSecretSnapshotRouter, action as eeActionRouter, organizations as eeOrganizationsRouter, - cloudProducts as eeCloudProductsRouter, -} from "./ee/routes/v1"; + cloudProducts as eeCloudProductsRouter +} from './ee/routes/v1'; import { signup as v1SignupRouter, auth as v1AuthRouter, @@ -47,8 +39,8 @@ import { stripe as v1StripeRouter, integration as v1IntegrationRouter, integrationAuth as v1IntegrationAuthRouter, - secretsFolder as v1SecretsFolder, -} from "./routes/v1"; + secretsFolder as v1SecretsFolder +} from './routes/v1'; import { signup as v2SignupRouter, auth as v2AuthRouter, @@ -62,130 +54,109 @@ import { apiKeyData as v2APIKeyDataRouter, environment as v2EnvironmentRouter, tags as v2TagsRouter, -} from "./routes/v2"; +} from './routes/v2'; import { auth as v3AuthRouter, secrets as v3SecretsRouter, signup as v3SignupRouter, - workspaces as v3WorkspacesRouter, -} from "./routes/v3"; -import { healthCheck } from "./routes/status"; -import { getLogger } from "./utils/logger"; -import { RouteNotFoundError } from "./utils/errors"; -import { requestErrorHandler } from "./middleware/requestErrorHandler"; + workspaces as v3WorkspacesRouter +} from './routes/v3'; +import { healthCheck } from './routes/status'; +import { getLogger } from './utils/logger'; +import { RouteNotFoundError } from './utils/errors'; +import { requestErrorHandler } from './middleware/requestErrorHandler'; import { - getMongoURL, getNodeEnv, getPort, - getSentryDSN, - getSiteURL, -} from "./config"; -import { initializePassport } from "./utils/auth"; + getSiteURL +} from './config'; +import { setup } from './utils/setup'; const main = async () => { - TelemetryService.logTelemetryMessage(); - setTransporter(await initSmtp()); + await setup(); await EELicenseService.initGlobalFeatureSet(); - 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(), - }); - } - - patchRouterParam(); const app = express(); - - await initializePassport(); - - app.enable("trust proxy"); + app.enable('trust proxy'); app.use(express.json()); app.use(cookieParser()); app.use( cors({ credentials: true, - origin: await getSiteURL(), + origin: await getSiteURL() }) ); app.use(requestIp.mw()); - if ((await getNodeEnv()) === "production") { + if ((await getNodeEnv()) === 'production') { // enable app-wide rate-limiting + helmet security // in production - app.disable("x-powered-by"); + app.disable('x-powered-by'); app.use(apiLimiter); app.use(helmet()); } // (EE) routes - app.use("/api/v1/secret", eeSecretRouter); - app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); - app.use("/api/v1/workspace", eeWorkspaceRouter); - app.use("/api/v1/action", eeActionRouter); - app.use("/api/v1/organizations", eeOrganizationsRouter); - app.use("/api/v1/cloud-products", eeCloudProductsRouter); + app.use('/api/v1/secret', eeSecretRouter); + app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter); + app.use('/api/v1/workspace', eeWorkspaceRouter); + app.use('/api/v1/action', eeActionRouter); + app.use('/api/v1/organizations', eeOrganizationsRouter); + app.use('/api/v1/cloud-products', eeCloudProductsRouter); // v1 routes (default) - app.use("/api/v1/signup", v1SignupRouter); - app.use("/api/v1/auth", v1AuthRouter); - app.use("/api/v1/bot", v1BotRouter); - app.use("/api/v1/user", v1UserRouter); - app.use("/api/v1/user-action", v1UserActionRouter); - app.use("/api/v1/organization", v1OrganizationRouter); - app.use("/api/v1/workspace", v1WorkspaceRouter); - app.use("/api/v1/membership-org", v1MembershipOrgRouter); - 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/password", v1PasswordRouter); - app.use("/api/v1/stripe", v1StripeRouter); - app.use("/api/v1/integration", v1IntegrationRouter); - app.use("/api/v1/integration-auth", v1IntegrationAuthRouter); - app.use("/api/v1/folder", v1SecretsFolder); + app.use('/api/v1/signup', v1SignupRouter); + app.use('/api/v1/auth', v1AuthRouter); + app.use('/api/v1/bot', v1BotRouter); + app.use('/api/v1/user', v1UserRouter); + app.use('/api/v1/user-action', v1UserActionRouter); + app.use('/api/v1/organization', v1OrganizationRouter); + app.use('/api/v1/workspace', v1WorkspaceRouter); + app.use('/api/v1/membership-org', v1MembershipOrgRouter); + 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); // 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); + app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); + app.use('/api/v1/folder', v1SecretsFolder) // v2 routes (improvements) - app.use("/api/v2/signup", v2SignupRouter); - app.use("/api/v2/auth", v2AuthRouter); - app.use("/api/v2/users", v2UsersRouter); - app.use("/api/v2/organizations", v2OrganizationsRouter); - 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/service-accounts", v2ServiceAccountsRouter); // new - app.use("/api/v2/api-key", v2APIKeyDataRouter); + app.use('/api/v2/signup', v2SignupRouter); + app.use('/api/v2/auth', v2AuthRouter); + app.use('/api/v2/users', v2UsersRouter); + app.use('/api/v2/organizations', v2OrganizationsRouter); + app.use('/api/v2/workspace', v2EnvironmentRouter); + app.use('/api/v2/workspace', v2TagsRouter); + app.use('/api/v2/workspace', v2WorkspaceRouter); + 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); // v3 routes (experimental) app.use("/api/v3/auth", v3AuthRouter); - app.use("/api/v3/secrets", v3SecretsRouter); - app.use("/api/v3/workspaces", v3WorkspacesRouter); + app.use('/api/v3/secrets', v3SecretsRouter); + app.use('/api/v3/workspaces', v3WorkspacesRouter); app.use("/api/v3/signup", v3SignupRouter); - // api docs - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); + // api docs + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) - // Server status - app.use("/api", healthCheck); + // server status + app.use('/api', healthCheck) //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next) => { if (res.headersSent) return next(); - next( - RouteNotFoundError({ - message: `The requested source '(${req.method})${req.url}' was not found`, - }) - ); - }); + next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` })) + }) app.use(requestErrorHandler); @@ -195,7 +166,7 @@ const main = async () => { ); }); - await createTestUserForDevelopment(); + // await createTestUserForDevelopment(); setUpHealthEndpoint(server); server.on("close", async () => { 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); 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/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 7e97b2e32..da517b998 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { Types } from 'mongoose'; -import { validateClientForWorkspace } from '../helpers/workspace'; +import { validateClientForWorkspace } from '../validation'; type req = 'params' | 'body' | 'query'; diff --git a/backend/src/models/backupPrivateKey.ts b/backend/src/models/backupPrivateKey.ts index 70dcd6475..09bcbb588 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,8 @@ export interface IBackupPrivateKey { iv: string; tag: string; salt: string; + algorithm: string; + keyEncoding: 'base64' | 'utf8'; verifier: string; } @@ -32,6 +39,19 @@ const backupPrivateKeySchema = new Schema( select: false, 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 + }, salt: { type: String, select: false, diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts index c7e5a9abe..5755bfd8e 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,8 @@ export interface IBot { encryptedPrivateKey: string; iv: string; tag: string; + algorithm: 'aes-256-gcm'; + keyEncoding: 'base64' | 'utf8'; } const botSchema = new Schema( @@ -45,6 +53,21 @@ const botSchema = new Schema( type: String, required: true, select: false + }, + algorithm: { // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + select: false + }, + keyEncoding: { + type: String, + enum: [ + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 + ], + required: true, + select: false } }, { diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index ead969fc7..affe811df 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,8 @@ export interface IIntegrationAuth extends Document { accessCiphertext?: string; accessIV?: string; accessTag?: string; + algorithm?: 'aes-256-gcm'; + keyEncoding?: 'utf8' | 'base64'; accessExpiresAt?: Date; } @@ -109,6 +114,19 @@ const integrationAuthSchema = new Schema( type: Date, select: 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 + } }, { timestamps: true, 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 47649dde4..885faaff6 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,8 @@ export interface ISecretBlindIndexData extends Document { encryptedSaltCiphertext: string; saltIV: string; saltTag: string; + algorithm: 'aes-256-gcm'; + keyEncoding: 'base64' | 'utf8' } const secretBlindIndexDataSchema = new Schema( @@ -15,7 +22,7 @@ const secretBlindIndexDataSchema = new Schema( ref: 'Workspace', required: true }, - encryptedSaltCiphertext: { + encryptedSaltCiphertext: { // TODO: make these select: false type: String, required: true }, @@ -26,7 +33,23 @@ const secretBlindIndexDataSchema = new Schema( saltTag: { type: String, required: true + }, + algorithm: { + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + select: false + }, + keyEncoding: { + type: String, + enum: [ + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64 + ], + 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/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/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 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..9c3877717 --- /dev/null +++ b/backend/src/utils/crypto/index.ts @@ -0,0 +1,166 @@ +import crypto from 'crypto'; +import nacl from 'tweetnacl'; +import util from 'tweetnacl-util'; +import { + IGenerateKeyPairOutput, + IEncryptAsymmetricInput, + IEncryptAsymmetricOutput, + IDecryptAsymmetricInput, + IEncryptSymmetricInput, + IDecryptSymmetricInput +} from '../../interfaces/utils'; +import { BadRequestError } from '../errors'; +import { + ALGORITHM_AES_256_GCM, + NONCE_BYTES_SIZE, + BLOCK_SIZE_BYTES_16 +} from '../../variables'; + +/** + * 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]. + * + * 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 - (hex) 128-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, + encryptSymmetric128BitHexKeyUTF8, + decryptSymmetric128BitHexKeyUTF8 +}; 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 new file mode 100644 index 000000000..c59a974d0 --- /dev/null +++ b/backend/src/utils/setup/index.ts @@ -0,0 +1,78 @@ +import * as Sentry from '@sentry/node'; +import { DatabaseService, TelemetryService } from '../../services'; +import { setTransporter } from '../../helpers/nodemailer'; +import { EELicenseService } from '../../ee/services'; +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 './backfillData'; +import { + reencryptBotPrivateKeys, + reencryptSecretBlindIndexDataSalts +} from './reencryptData'; +import { + getNodeEnv, + getMongoURL, + getSentryDSN +} from '../../config'; + +/** + * Prepare Infisical upon startup. This includes tasks like: + * - Log initial telemetry message + * - Initializing SMTP configuration + * - Initializing the instance global feature set (if applicable) + * - 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 global feature set + await EELicenseService.initGlobalFeatureSet(); + + // 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 to catch up with new collections and updated fields + await backfillSecretVersions(); + await backfillBots(); + await backfillSecretBlindIndexData(); + await backfillEncryptionMetadata(); + + // 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()) + }); + + await createTestUserForDevelopment(); +} + diff --git a/backend/src/utils/setup/reencryptData.ts b/backend/src/utils/setup/reencryptData.ts new file mode 100644 index 000000000..0741b44f4 --- /dev/null +++ b/backend/src/utils/setup/reencryptData.ts @@ -0,0 +1,124 @@ +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(); + + 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..5e7aa1e5b --- /dev/null +++ b/backend/src/utils/setup/validateConfig.ts @@ -0,0 +1,61 @@ +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(); + + 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/index.ts b/backend/src/validation/index.ts new file mode 100644 index 000000000..84f25bb74 --- /dev/null +++ b/backend/src/validation/index.ts @@ -0,0 +1,10 @@ +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..1cbdca420 --- /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, workspace }); + } + + 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, workspace }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for workspace' + }); +} 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..bd2ae110c --- /dev/null +++ b/backend/src/variables/crypto.ts @@ -0,0 +1,7 @@ +export const ALGORITHM_AES_256_GCM = 'aes-256-gcm'; +export const NONCE_BYTES_SIZE = 12; +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 450c2fedd..281145a99 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -1,154 +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, - SMTP_HOST_GMAIL -} 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, - SMTP_HOST_GMAIL, - 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'; 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 3ec46c4e7..f5dd05d0b 100644 --- a/backend/src/variables/smtp.ts +++ b/backend/src/variables/smtp.ts @@ -1,13 +1,5 @@ -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'; -const SMTP_HOST_GMAIL = 'smtp.gmail.com'; - -export { - SMTP_HOST_SENDGRID, - SMTP_HOST_MAILGUN, - SMTP_HOST_SOCKETLABS, - SMTP_HOST_ZOHOMAIL, - SMTP_HOST_GMAIL -} \ 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'; +export const SMTP_HOST_GMAIL = 'smtp.gmail.com'; 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/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/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, }); 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'); - }); - }); }); 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) 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 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 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/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index 3aa2e5890..bf31c518d 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -137,7 +137,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); } 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
} 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..938d9e428 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -261,9 +261,10 @@ 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 max-h-96 border-gray-700" > - {workspaces +
+ {workspaces .filter((ws) => ws.organization === currentOrg?._id) .map(({ _id, name }) => ( { {name} ))} - {/*
*/} +
+