Merge branch 'feature/google-signin-signup-integration' of https://github.com/sheensantoscapadngan/infisical into feature/google-signin-signup-integration

This commit is contained in:
Vladyslav Matsiiako
2023-05-28 14:22:42 -07:00
97 changed files with 2600 additions and 2129 deletions
+12 -11
View File
@@ -13,6 +13,7 @@ jobs:
check-be-pr: check-be-pr:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
@@ -26,17 +27,17 @@ jobs:
- name: 📦 Install dependencies - name: 📦 Install dependencies
run: npm ci --only-production run: npm ci --only-production
working-directory: backend working-directory: backend
- name: 🧪 Run tests # - name: 🧪 Run tests
run: npm run test:ci # run: npm run test:ci
working-directory: backend # working-directory: backend
- name: 📁 Upload test results # - name: 📁 Upload test results
uses: actions/upload-artifact@v3 # uses: actions/upload-artifact@v3
if: always() # if: always()
with: # with:
name: be-test-results # name: be-test-results
path: | # path: |
./backend/reports # ./backend/reports
./backend/coverage # ./backend/coverage
- name: 🏗️ Run build - name: 🏗️ Run build
run: npm run build run: npm run build
working-directory: backend working-directory: backend
+12 -17
View File
@@ -2,40 +2,35 @@ name: Check Frontend Pull Request
on: on:
pull_request: pull_request:
types: [ opened, synchronize ] types: [opened, synchronize]
paths: paths:
- 'frontend/**' - "frontend/**"
- '!frontend/README.md' - "!frontend/README.md"
- '!frontend/.*' - "!frontend/.*"
- 'frontend/.eslintrc.js' - "frontend/.eslintrc.js"
jobs: jobs:
check-fe-pr: check-fe-pr:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- - name: ☁️ Checkout source
name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- - name: 🔧 Setup Node 16
name: 🔧 Setup Node 16
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: "16"
cache: 'npm' cache: "npm"
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
- - name: 📦 Install dependencies
name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts run: npm ci --only-production --ignore-scripts
working-directory: frontend working-directory: frontend
# - # -
# name: 🧪 Run tests # name: 🧪 Run tests
# run: npm run test:ci # run: npm run test:ci
# working-directory: frontend # working-directory: frontend
- - name: 🏗️ Run build
name: 🏗️ Run build
run: npm run build run: npm run build
working-directory: frontend working-directory: frontend
+2 -2
View File
@@ -25,7 +25,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" /> <img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a> </a>
<a href="https://cloudsmith.io/~infisical/repos/"> <a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-150.8k-orange" alt="Cloudsmith downloads" /> <img src="https://img.shields.io/badge/Downloads-240.2k-orange" alt="Cloudsmith downloads" />
</a> </a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g"> <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" /> <img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="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 - **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
- **Role-based Access Controls** per environment - **Role-based Access Controls** per environment
- [**Simple on-premise deployments** to AWS and Digital Ocean](https://infisical.com/docs/self-hosting/overview) - [**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. And much more.
+1 -1
View File
@@ -33,7 +33,7 @@
"express-validator": "^6.14.2", "express-validator": "^6.14.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.1.3", "infisical-node": "^1.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
+1 -1
View File
@@ -24,7 +24,7 @@
"express-validator": "^6.14.2", "express-validator": "^6.14.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.1.3", "infisical-node": "^1.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
+17 -4
View File
@@ -1,12 +1,19 @@
import InfisicalClient from 'infisical-node'; import InfisicalClient from 'infisical-node';
const client = new InfisicalClient({ export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN! token: process.env.INFISICAL_TOKEN!
}); });
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000; 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 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 getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d'; export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue; 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 getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical'; export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
export const getLicenseKey = async () => (await client.getSecret('LICENSE_KEY')).secretValue; export const getLicenseKey = async () => {
export const getLicenseServerKey = async () => (await client.getSecret('LICENSE_SERVER_KEY')).secretValue; 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'; export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
// TODO: deprecate from here // TODO: deprecate from here
@@ -5,7 +5,7 @@ import {
IntegrationAuth, IntegrationAuth,
Bot Bot
} from '../../models'; } 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 { IntegrationService } from '../../services';
import { import {
getApps, getApps,
@@ -129,7 +129,9 @@ export const saveIntegrationAccessToken = async (
integration integration
}, { }, {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
integration integration,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, { }, {
new: true, new: true,
upsert: true upsert: true
@@ -39,7 +39,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
error: 'Failed to send email verification code' error: 'Failed to send email verification code'
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: `Sent an email verification code to ${email}` message: `Sent an email verification code to ${email}`
}); });
@@ -6,7 +6,7 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre
const { ValidationError } = mongoose.Error; const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb'; 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 { TelemetryService } from '../../services';
import { User } from "../../models"; import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors'; import { AccountNotFoundError } from '../../utils/errors';
@@ -36,7 +36,9 @@ export const createSecret = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type: secretToCreate.type, 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), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type: rawSecret.type, 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) sanitizedSecretesToCreate.push(safeUpdateFields)
+50 -21
View File
@@ -7,7 +7,9 @@ import {
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS ACTION_DELETE_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../../variables'; } from '../../variables';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors'; import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors';
import { EventService } from '../../services'; import { EventService } from '../../services';
@@ -51,7 +53,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (!workspace) throw WorkspaceNotFoundError(); if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1; const isPaid = orgPlan.tier >= 1;
const createSecrets: BatchSecret[] = []; const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = [];
@@ -85,7 +87,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
path: fullFolderPath, path: fullFolderPath,
folder: folderId, folder: folderId,
secretBlindIndex secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}); });
break; break;
case 'PATCH': case 'PATCH':
@@ -100,6 +104,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
secretBlindIndex, secretBlindIndex,
folder: folderId, folder: folderId,
path: fullFolderPath, path: fullFolderPath,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}); });
break; break;
case 'DELETE': case 'DELETE':
@@ -144,7 +150,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel, channel,
userAgent: req.headers?.['user-agent'], 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, secretCommentCiphertext: u.secretCommentCiphertext,
secretCommentIV: u.secretCommentIV, secretCommentIV: u.secretCommentIV,
secretCommentTag: u.secretCommentTag, secretCommentTag: u.secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags tags: u.tags
})); }));
@@ -232,7 +241,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel, channel,
userAgent: req.headers?.['user-agent'], userAgent: req.headers?.['user-agent'],
isPaid isPaid,
email: req.user.email
} }
}); });
} }
@@ -268,7 +278,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel: channel, channel: channel,
userAgent: req.headers?.['user-agent'], 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(); if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1; const isPaid = orgPlan.tier >= 1;
let listOfSecretsToCreate; let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) { if (Array.isArray(req.body.secrets)) {
@@ -457,6 +468,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags tags
}); });
}) })
@@ -503,7 +516,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, 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, workspaceId,
channel: channel, channel: channel,
userAgent: req.headers?.['user-agent'], 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(); if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1; const isPaid = orgPlan.tier >= 1;
// secrets to return // secrets to return
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
@@ -748,7 +766,10 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel, channel,
userAgent: req.headers?.['user-agent'], 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, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags, tags,
...(( ...((
secretCommentCiphertext !== undefined && secretCommentCiphertext !== undefined &&
@@ -905,6 +928,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags: tags ? tags : secret.tags tags: tags ? tags : secret.tags
}); });
}) })
@@ -963,7 +988,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
if (!workspace) throw WorkspaceNotFoundError(); if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1; const isPaid = orgPlan.tier >= 1;
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
@@ -978,7 +1003,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: key, workspaceId: key,
channel: channel, channel: channel,
userAgent: req.headers?.['user-agent'], 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) workspaceId: new Types.ObjectId(key)
}); });
const organizationId = ( const workspace = await Workspace.findById(key);
await Workspace.findOne({ if (!workspace) throw WorkspaceNotFoundError();
_id: key
}) const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
)?.organization?.toString(); const isPaid = orgPlan.tier >= 1;
const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || '');
const isPaid = orgPlan.slug != 'starter';
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
@@ -1121,7 +1147,10 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key, workspaceId: key,
channel: channel, channel: channel,
userAgent: req.headers?.['user-agent'], userAgent: req.headers?.['user-agent'],
isPaid isPaid,
email: await TelemetryService.getDistinctId({
authData: req.authData
})
} }
}); });
} }
@@ -7,13 +7,12 @@ import { EELicenseService } from '../../services';
* Return the organization's current plan and allowed feature set * Return the organization's current plan and allowed feature set
*/ */
export const getOrganizationPlan = async (req: Request, res: Response) => { 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 const plan = await EELicenseService.getOrganizationPlan(organizationId);
EELicenseService.localFeatureSet.set(req.organization._id.toString(), plan);
return res.status(200).send({ return res.status(200).send({
plan plan,
}); });
} }
@@ -162,6 +162,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
algorithm,
keyEncoding
} = oldSecretVersion; } = oldSecretVersion;
// update secret // update secret
@@ -182,6 +184,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
algorithm,
keyEncoding
}, },
{ {
new: true new: true
@@ -205,7 +209,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
algorithm,
keyEncoding
}).save(); }).save();
// take secret snapshot // take secret snapshot
+1 -45
View File
@@ -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 { export {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper, addSecretVersionsHelper,
markDeletedSecretVersionsHelper, markDeletedSecretVersionsHelper
initSecretVersioningHelper,
}; };
+19 -1
View File
@@ -2,6 +2,9 @@ import { Schema, model, Types } from 'mongoose';
import { import {
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../../variables'; } from '../../variables';
export interface ISecretVersion { export interface ISecretVersion {
@@ -20,6 +23,8 @@ export interface ISecretVersion {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'utf8' | 'base64';
} }
const secretVersionSchema = new Schema<ISecretVersion>( const secretVersionSchema = new Schema<ISecretVersion>(
@@ -85,7 +90,20 @@ const secretVersionSchema = new Schema<ISecretVersion>(
secretValueTag: { secretValueTag: {
type: String, // symmetric type: String, // symmetric
required: true 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 timestamps: true
+15 -6
View File
@@ -17,9 +17,11 @@ import { OrganizationNotFoundError } from '../../utils/errors';
interface FeatureSet { interface FeatureSet {
_id: string | null; _id: string | null;
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null; slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
tier: number | null; tier: number;
projectLimit: number | null; workspaceLimit: number | null;
workspacesUsed: number;
memberLimit: number | null; memberLimit: number | null;
membersUsed: number;
secretVersioning: boolean; secretVersioning: boolean;
pitRecovery: boolean; pitRecovery: boolean;
rbac: boolean; rbac: boolean;
@@ -44,8 +46,10 @@ class EELicenseService {
_id: null, _id: null,
slug: null, slug: null,
tier: -1, tier: -1,
projectLimit: null, workspaceLimit: null,
workspacesUsed: 0,
memberLimit: null, memberLimit: null,
membersUsed: 0,
secretVersioning: true, secretVersioning: true,
pitRecovery: true, pitRecovery: true,
rbac: true, rbac: true,
@@ -63,11 +67,13 @@ class EELicenseService {
}); });
} }
public async getOrganizationPlan(organizationId: string) { public async getOrganizationPlan(organizationId: string): Promise<FeatureSet> {
try { try {
if (this.instanceType === 'cloud') { if (this.instanceType === 'cloud') {
const cachedPlan = this.localFeatureSet.get(organizationId); const cachedPlan = this.localFeatureSet.get<FeatureSet>(organizationId);
if (cachedPlan) return cachedPlan; if (cachedPlan) {
return cachedPlan;
}
const organization = await Organization.findById(organizationId); const organization = await Organization.findById(organizationId);
if (!organization) throw OrganizationNotFoundError(); if (!organization) throw OrganizationNotFoundError();
@@ -76,6 +82,9 @@ class EELicenseService {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
); );
// cache fetched plan for organization
this.localFeatureSet.set(organizationId, currentPlan);
return currentPlan; return currentPlan;
} }
} catch (err) { } catch (err) {
+1 -11
View File
@@ -3,8 +3,7 @@ import { ISecretVersion } from '../models';
import { import {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper, addSecretVersionsHelper,
markDeletedSecretVersionsHelper, markDeletedSecretVersionsHelper
initSecretVersioningHelper
} from '../helpers/secret'; } from '../helpers/secret';
import EELicenseService from './EELicenseService'; import EELicenseService from './EELicenseService';
@@ -64,15 +63,6 @@ class EESecretService {
secretIds 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; export default EESecretService;
-1
View File
@@ -42,7 +42,6 @@ const validateAuthMode = ({
headers: { [key: string]: string | string[] | undefined }, headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[] acceptedAuthModes: string[]
}) => { }) => {
// TODO: refactor middleware
const apiKey = headers['x-api-key']; const apiKey = headers['x-api-key'];
const authHeader = headers['authorization']; const authHeader = headers['authorization'];
+98 -131
View File
@@ -4,107 +4,26 @@ import {
BotKey, BotKey,
Secret, Secret,
ISecret, ISecret,
IUser, IUser
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
} from "../models"; } from "../models";
import { import {
generateKeyPair, generateKeyPair,
encryptSymmetric, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric, decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric, decryptAsymmetric
} from "../utils/crypto"; } from '../utils/crypto';
import { import {
SECRET_SHARED, SECRET_SHARED,
AUTH_MODE_JWT, ALGORITHM_AES_256_GCM,
AUTH_MODE_SERVICE_ACCOUNT, ENCODING_SCHEME_UTF8,
AUTH_MODE_SERVICE_TOKEN, ENCODING_SCHEME_BASE64
AUTH_MODE_API_KEY,
} from "../variables"; } from "../variables";
import { getEncryptionKey } from "../config"; import {
import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors"; getEncryptionKey,
import { validateMembership } from "../helpers/membership"; getRootEncryptionKey,
import { validateUserClientForWorkspace } from "../helpers/user"; client
import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount"; } from "../config";
import { InternalServerError } from "../utils/errors";
/**
* 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",
});
};
/** /**
* Create an inactive bot with name [name] for workspace with id [workspaceId] * Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -119,23 +38,52 @@ const createBot = async ({
name: string; name: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair(); const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey, if (rootEncryptionKey) {
key: await getEncryptionKey(), 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) => { secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({ const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
tag: secret.secretKeyTag, tag: secret.secretKeyTag,
key, key,
}); });
const secretValue = decryptSymmetric({ const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext, ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV, iv: secret.secretValueIV,
tag: secret.secretValueTag, tag: secret.secretValueTag,
@@ -189,34 +137,54 @@ const getSecretsHelper = async ({
* @returns {String} key - decrypted workspace key * @returns {String} key - decrypted workspace key
*/ */
const getKey = async ({ workspaceId }: { workspaceId: string }) => { const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({ const botKey = await BotKey.findOne({
workspace: workspaceId, workspace: workspaceId,
}).populate<{ sender: IUser }>("sender", "publicKey"); })
.populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key"); if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: workspaceId, workspace: workspaceId,
}).select("+encryptedPrivateKey +iv +tag"); }).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
if (!bot) throw new Error("Failed to find bot"); if (!bot) throw new Error("Failed to find bot");
if (!bot.isActive) throw new Error("Bot is not active"); if (!bot.isActive) throw new Error("Bot is not active");
const privateKeyBot = decryptSymmetric({ if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
ciphertext: bot.encryptedPrivateKey, // case: encoding scheme is base64
iv: bot.iv, const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
tag: bot.tag,
key: await getEncryptionKey(),
});
const key = decryptAsymmetric({ return decryptAsymmetric({
ciphertext: botKey.encryptedKey, ciphertext: botKey.encryptedKey,
nonce: botKey.nonce, nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string, publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot, 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; plaintext: string;
}) => { }) => {
const key = await getKey({ workspaceId: workspaceId.toString() }); const key = await getKey({ workspaceId: workspaceId.toString() });
const { ciphertext, iv, tag } = encryptSymmetric({ const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext, plaintext,
key, key,
}); });
@@ -266,7 +234,7 @@ const decryptSymmetricHelper = async ({
tag: string; tag: string;
}) => { }) => {
const key = await getKey({ workspaceId: workspaceId.toString() }); const key = await getKey({ workspaceId: workspaceId.toString() });
const plaintext = decryptSymmetric({ const plaintext = decryptSymmetric128BitHexKeyUTF8({
ciphertext, ciphertext,
iv, iv,
tag, tag,
@@ -277,9 +245,8 @@ const decryptSymmetricHelper = async ({
}; };
export { export {
validateClientForBot,
createBot, createBot,
getSecretsHelper, getSecretsHelper,
encryptSymmetricHelper, encryptSymmetricHelper,
decryptSymmetricHelper, decryptSymmetricHelper
}; };
+1 -5
View File
@@ -1,6 +1,4 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { EESecretService } from '../ee/services';
import { SecretService } from '../services';
import { getLogger } from '../utils/logger'; import { getLogger } from '../utils/logger';
/** /**
@@ -21,9 +19,7 @@ const initDatabaseHelper = async ({
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string'); mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
(await getLogger("database")).info("Database connection established"); (await getLogger("database")).info("Database connection established");
await EESecretService.initSecretVersioning();
await SecretService.initSecretBlindIndexDataHelper();
} catch (err) { } catch (err) {
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`); (await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
} }
+10 -105
View File
@@ -3,40 +3,20 @@ import { Types } from 'mongoose';
import { import {
Bot, Bot,
Integration, Integration,
IntegrationAuth, IntegrationAuth
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models'; } from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services'; import { BotService } from '../services';
import { import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables'; } from '../variables';
import { import {
UnauthorizedRequestError, UnauthorizedRequestError,
IntegrationAuthNotFoundError,
IntegrationNotFoundError
} from '../utils/errors'; } from '../utils/errors';
import RequestError from '../utils/requestError'; 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 { interface Update {
workspace: string; workspace: string;
@@ -45,84 +25,6 @@ interface Update {
accountId?: string; 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 * Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
* named [integration] * named [integration]
@@ -400,7 +302,9 @@ const setIntegrationAuthRefreshHelper = async ({
}, { }, {
refreshCiphertext: obj.ciphertext, refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv, refreshIV: obj.iv,
refreshTag: obj.tag refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, { }, {
new: true new: true
}); });
@@ -461,7 +365,9 @@ const setIntegrationAuthAccessHelper = async ({
accessCiphertext: encryptedAccessTokenObj.ciphertext, accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv, accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag, accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, { }, {
new: true new: true
}); });
@@ -475,7 +381,6 @@ const setIntegrationAuthAccessHelper = async ({
} }
export { export {
validateClientForIntegration,
handleOAuthExchangeHelper, handleOAuthExchangeHelper,
syncIntegrationsHelper, syncIntegrationsHelper,
getIntegrationAuthRefreshHelper, getIntegrationAuthRefreshHelper,
+2 -96
View File
@@ -2,105 +2,12 @@ import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Membership, Membership,
Key, Key
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models'; } from '../models';
import { import {
MembershipNotFoundError, MembershipNotFoundError,
BadRequestError, BadRequestError
UnauthorizedRequestError
} from '../utils/errors'; } 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] * 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 { export {
validateClientForMembership,
validateMembership, validateMembership,
addMemberships, addMemberships,
findMembership, findMembership,
+1 -85
View File
@@ -3,95 +3,12 @@ import {
MembershipOrg, MembershipOrg,
Workspace, Workspace,
Membership, Membership,
Key, Key
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models'; } from '../models';
import { import {
MembershipOrgNotFoundError, MembershipOrgNotFoundError,
BadRequestError,
UnauthorizedRequestError UnauthorizedRequestError
} from '../utils/errors'; } 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] * Validate that user with id [userId] is a member of organization with id [organizationId]
@@ -234,7 +151,6 @@ const deleteMembershipOrg = async ({
}; };
export { export {
validateClientForMembershipOrg,
validateMembershipOrg, validateMembershipOrg,
findMembershipOrg, findMembershipOrg,
addMembershipsOrg, addMembershipsOrg,
+4 -104
View File
@@ -1,21 +1,8 @@
import Stripe from "stripe"; import Stripe from "stripe";
import { Types } from "mongoose"; import { Types } from "mongoose";
import {
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
} from "../models";
import { Organization, MembershipOrg } from "../models"; import { Organization, MembershipOrg } from "../models";
import { import {
ACCEPTED, ACCEPTED
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER,
} from "../variables"; } from "../variables";
import { import {
getStripeSecretKey, getStripeSecretKey,
@@ -23,12 +10,6 @@ import {
getStripeProductTeam, getStripeProductTeam,
getStripeProductStarter, getStripeProductStarter,
} from "../config"; } from "../config";
import {
UnauthorizedRequestError,
OrganizationNotFoundError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "../helpers/user";
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
import { import {
EELicenseService EELicenseService
} from '../ee/services'; } from '../ee/services';
@@ -40,88 +21,6 @@ import {
licenseKeyRequest licenseKeyRequest
} from '../config/request'; } 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] * Create an organization with name [name]
* @param {Object} obj * @param {Object} obj
@@ -251,6 +150,8 @@ const updateSubscriptionOrgQuantity = async ({
quantity quantity
} }
); );
EELicenseService.localFeatureSet.del(organizationId);
} }
if (EELicenseService.instanceType === 'enterprise-self-hosted') { if (EELicenseService.instanceType === 'enterprise-self-hosted') {
@@ -273,8 +174,7 @@ const updateSubscriptionOrgQuantity = async ({
}; };
export { export {
validateClientForOrganization,
createOrganization, createOrganization,
initSubscriptionOrg, initSubscriptionOrg,
updateSubscriptionOrgQuantity, updateSubscriptionOrgQuantity
}; };
+14
View File
@@ -9,6 +9,8 @@ import {
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
} from "../variables"; } from "../variables";
import _ from "lodash"; import _ from "lodash";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
@@ -194,6 +196,8 @@ const v1PushSecrets = async ({
secretValueIV: newSecret.ivValue, secretValueIV: newSecret.ivValue,
secretValueTag: newSecret.tagValue, secretValueTag: newSecret.tagValue,
secretValueHash: newSecret.hashValue, secretValueHash: newSecret.hashValue,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}); });
}), }),
}); });
@@ -225,6 +229,8 @@ const v1PushSecrets = async ({
secretCommentIV: s.ivComment, secretCommentIV: s.ivComment,
secretCommentTag: s.tagComment, secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment, secretCommentHash: s.hashComment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}; };
if (toAdd[idx].type === "personal") { if (toAdd[idx].type === "personal") {
@@ -254,6 +260,8 @@ const v1PushSecrets = async ({
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash, secretValueHash,
algorithm,
keyEncoding
}) => }) =>
new SecretVersion({ new SecretVersion({
secret: _id, secret: _id,
@@ -271,6 +279,8 @@ const v1PushSecrets = async ({
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash, secretValueHash,
algorithm,
keyEncoding
}) })
), ),
}); });
@@ -467,6 +477,8 @@ const v2PushSecrets = async ({
workspace: workspaceId, workspace: workspaceId,
type: toAdd[idx].type, type: toAdd[idx].type,
environment, environment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
...(toAdd[idx].type === "personal" ? { user: userId } : {}), ...(toAdd[idx].type === "personal" ? { user: userId } : {}),
})) }))
); );
@@ -478,6 +490,8 @@ const v2PushSecrets = async ({
...secretDocument, ...secretDocument,
secret: secretDocument._id, secret: secretDocument._id,
isDeleted: false, isDeleted: false,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}); });
}), }),
}); });
+84 -262
View File
@@ -7,58 +7,35 @@ import {
DeleteSecretParams DeleteSecretParams
} from '../interfaces/services/SecretService'; } from '../interfaces/services/SecretService';
import { import {
AuthData
} from '../interfaces/middleware';
import {
User,
Workspace,
ServiceAccount,
ServiceTokenData,
Secret, Secret,
ISecret, ISecret,
SecretBlindIndexData, SecretBlindIndexData,
} from '../models'; } from '../models';
import { SecretVersion } from '../ee/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 { import {
BadRequestError, BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError, SecretNotFoundError,
SecretBlindIndexDataNotFoundError SecretBlindIndexDataNotFoundError,
InternalServerError
} from '../utils/errors'; } from '../utils/errors';
import { import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS ACTION_DELETE_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables'; } from '../variables';
import crypto from 'crypto'; import crypto from 'crypto';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
import { import {
encryptSymmetric, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric decryptSymmetric128BitHexKeyUTF8
} from '../utils/crypto'; } from '../utils/crypto';
import { getEncryptionKey } from '../config'; import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
import { TelemetryService } from '../services'; import { TelemetryService } from '../services';
import { import {
EESecretService, EESecretService,
@@ -69,199 +46,6 @@ import {
getAuthDataPayloadUserObj getAuthDataPayloadUserObj
} from '../utils/auth'; } 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] * Create secret blind index data containing encrypted blind index [salt]
* for workspace with id [workspaceId] * for workspace with id [workspaceId]
@@ -273,26 +57,47 @@ const createSecretBlindIndexDataHelper = async ({
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
// initialize random blind index salt for workspace // initialize random blind index salt for workspace
const salt = crypto.randomBytes(16).toString('base64'); const salt = crypto.randomBytes(16).toString('base64');
const { const encryptionKey = await getEncryptionKey();
ciphertext: encryptedSaltCiphertext, const rootEncryptionKey = await getRootEncryptionKey();
iv: saltIV,
tag: saltTag if (rootEncryptionKey) {
} = encryptSymmetric({ const {
plaintext: salt, ciphertext: encryptedSaltCiphertext,
key: await getEncryptionKey() iv: saltIV,
}); tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey);
const secretBlindIndexData = await new SecretBlindIndexData({
workspace: workspaceId, return await new SecretBlindIndexData({
encryptedSaltCiphertext, workspace: workspaceId,
saltIV, encryptedSaltCiphertext,
saltTag saltIV,
}).save(); saltTag,
algorithm: ALGORITHM_AES_256_GCM,
return secretBlindIndexData; 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; workspaceId: Types.ObjectId;
}) => { }) => {
// check if workspace blind index data exists
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId workspace: workspaceId
}); }).select('+algorithm +keyEncoding');
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
const salt = decryptSymmetric({ return client.decryptSymmetric(
ciphertext: secretBlindIndexData.encryptedSaltCiphertext, secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV, rootEncryptionKey,
tag: secretBlindIndexData.saltTag, secretBlindIndexData.saltIV,
key: await getEncryptionKey() 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(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt // decrypt workspace salt
const salt = decryptSymmetric({ const salt = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext, ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV, iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag, tag: secretBlindIndexData.saltTag,
@@ -464,7 +283,9 @@ const createSecretHelper = async ({
secretValueTag, secretValueTag,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save(); }).save();
const secretVersion = new SecretVersion({ const secretVersion = new SecretVersion({
@@ -481,7 +302,9 @@ const createSecretHelper = async ({
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}); });
// // (EE) add version for new secret // // (EE) add version for new secret
@@ -771,7 +594,9 @@ const updateSecretHelper = async ({
secretKeyTag: secret.secretKeyTag, secretKeyTag: secret.secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}); });
// (EE) add version for new secret // (EE) add version for new secret
@@ -932,9 +757,6 @@ const deleteSecretHelper = async ({
} }
export { export {
validateClientForSecret,
validateClientForSecrets,
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper, createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper, getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper, generateSecretBlindIndexWithSaltHelper,
+4 -222
View File
@@ -1,24 +1,8 @@
import { Types } from 'mongoose';
import { import {
IUser, IUser,
ISecret,
IServiceAccount,
User, User,
Membership,
IOrganization,
Organization,
} from '../models'; } from '../models';
import { sendMail } from './nodemailer'; 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] * Initialize a user under email [email]
@@ -26,7 +10,7 @@ import {
* @param {String} obj.email - email of user to initialize * @param {String} obj.email - email of user to initialize
* @returns {Object} user - the initialized user * @returns {Object} user - the initialized user
*/ */
const setupAccount = async ({ email }: { email: string }) => { export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({ const user = await new User({
email email
}).save(); }).save();
@@ -52,7 +36,7 @@ const setupAccount = async ({ email }: { email: string }) => {
* @param {String} obj.verifier - verifier for auth SRP * @param {String} obj.verifier - verifier for auth SRP
* @returns {Object} user - the completed user * @returns {Object} user - the completed user
*/ */
const completeAccount = async ({ export const completeAccount = async ({
userId, userId,
firstName, firstName,
lastName, lastName,
@@ -113,7 +97,7 @@ const completeAccount = async ({
* @param {String} obj.ip - login ip address * @param {String} obj.ip - login ip address
* @param {String} obj.userAgent - login user-agent * @param {String} obj.userAgent - login user-agent
*/ */
const checkUserDevice = async ({ export const checkUserDevice = async ({
user, user,
ip, ip,
userAgent 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
};
+1 -124
View File
@@ -1,136 +1,14 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { Types } from 'mongoose';
import { import {
Workspace, Workspace,
Bot, Bot,
Membership, Membership,
Key, Key,
Secret, Secret
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
SecretBlindIndexData
} from '../models'; } from '../models';
import { createBot } from '../helpers/bot'; 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'; 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] * Create a workspace with name [name] in organization with id [organizationId]
* and a bot for it. * and a bot for it.
@@ -203,7 +81,6 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
}; };
export { export {
validateClientForWorkspace,
createWorkspace, createWorkspace,
deleteWorkspace deleteWorkspace
}; };
+74 -104
View File
@@ -1,34 +1,26 @@
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
import express from "express"; import express from 'express';
import helmet from "helmet"; import helmet from 'helmet';
import cors from "cors"; import cors from 'cors';
import * as Sentry from "@sentry/node"; import { DatabaseService } from './services';
import { DatabaseService } from "./services"; import { EELicenseService } from './ee/services';
import { EELicenseService } from "./ee/services"; import { setUpHealthEndpoint } from './services/health';
import { setUpHealthEndpoint } from "./services/health"; import cookieParser from 'cookie-parser';
import { initSmtp } from "./services/smtp"; import swaggerUi = require('swagger-ui-express');
import { TelemetryService } from "./services";
import { setTransporter } from "./helpers/nodemailer";
import { createTestUserForDevelopment } from "./utils/addDevelopmentUser";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require("./utils/patchAsyncRoutes"); const swaggerFile = require('../spec.json');
import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express");
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json"); const requestIp = require('request-ip');
// eslint-disable-next-line @typescript-eslint/no-var-requires import { apiLimiter } from './helpers/rateLimiter';
const requestIp = require("request-ip");
import { apiLimiter } from "./helpers/rateLimiter";
import { import {
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
secret as eeSecretRouter, secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter, action as eeActionRouter,
organizations as eeOrganizationsRouter, organizations as eeOrganizationsRouter,
cloudProducts as eeCloudProductsRouter, cloudProducts as eeCloudProductsRouter
} from "./ee/routes/v1"; } from './ee/routes/v1';
import { import {
signup as v1SignupRouter, signup as v1SignupRouter,
auth as v1AuthRouter, auth as v1AuthRouter,
@@ -47,8 +39,8 @@ import {
stripe as v1StripeRouter, stripe as v1StripeRouter,
integration as v1IntegrationRouter, integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter, integrationAuth as v1IntegrationAuthRouter,
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder
} from "./routes/v1"; } from './routes/v1';
import { import {
signup as v2SignupRouter, signup as v2SignupRouter,
auth as v2AuthRouter, auth as v2AuthRouter,
@@ -62,130 +54,109 @@ import {
apiKeyData as v2APIKeyDataRouter, apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter, environment as v2EnvironmentRouter,
tags as v2TagsRouter, tags as v2TagsRouter,
} from "./routes/v2"; } from './routes/v2';
import { import {
auth as v3AuthRouter, auth as v3AuthRouter,
secrets as v3SecretsRouter, secrets as v3SecretsRouter,
signup as v3SignupRouter, signup as v3SignupRouter,
workspaces as v3WorkspacesRouter, workspaces as v3WorkspacesRouter
} from "./routes/v3"; } from './routes/v3';
import { healthCheck } from "./routes/status"; import { healthCheck } from './routes/status';
import { getLogger } from "./utils/logger"; import { getLogger } from './utils/logger';
import { RouteNotFoundError } from "./utils/errors"; import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from "./middleware/requestErrorHandler"; import { requestErrorHandler } from './middleware/requestErrorHandler';
import { import {
getMongoURL,
getNodeEnv, getNodeEnv,
getPort, getPort,
getSentryDSN, getSiteURL
getSiteURL, } from './config';
} from "./config"; import { setup } from './utils/setup';
import { initializePassport } from "./utils/auth";
const main = async () => { const main = async () => {
TelemetryService.logTelemetryMessage(); await setup();
setTransporter(await initSmtp());
await EELicenseService.initGlobalFeatureSet(); 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(); const app = express();
app.enable('trust proxy');
await initializePassport();
app.enable("trust proxy");
app.use(express.json()); app.use(express.json());
app.use(cookieParser()); app.use(cookieParser());
app.use( app.use(
cors({ cors({
credentials: true, credentials: true,
origin: await getSiteURL(), origin: await getSiteURL()
}) })
); );
app.use(requestIp.mw()); app.use(requestIp.mw());
if ((await getNodeEnv()) === "production") { if ((await getNodeEnv()) === 'production') {
// enable app-wide rate-limiting + helmet security // enable app-wide rate-limiting + helmet security
// in production // in production
app.disable("x-powered-by"); app.disable('x-powered-by');
app.use(apiLimiter); app.use(apiLimiter);
app.use(helmet()); app.use(helmet());
} }
// (EE) routes // (EE) routes
app.use("/api/v1/secret", eeSecretRouter); app.use('/api/v1/secret', eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use("/api/v1/workspace", eeWorkspaceRouter); app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use("/api/v1/action", eeActionRouter); app.use('/api/v1/action', eeActionRouter);
app.use("/api/v1/organizations", eeOrganizationsRouter); app.use('/api/v1/organizations', eeOrganizationsRouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use('/api/v1/cloud-products', eeCloudProductsRouter);
// v1 routes (default) // v1 routes (default)
app.use("/api/v1/signup", v1SignupRouter); app.use('/api/v1/signup', v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter); app.use('/api/v1/auth', v1AuthRouter);
app.use("/api/v1/bot", v1BotRouter); app.use('/api/v1/bot', v1BotRouter);
app.use("/api/v1/user", v1UserRouter); app.use('/api/v1/user', v1UserRouter);
app.use("/api/v1/user-action", v1UserActionRouter); app.use('/api/v1/user-action', v1UserActionRouter);
app.use("/api/v1/organization", v1OrganizationRouter); app.use('/api/v1/organization', v1OrganizationRouter);
app.use("/api/v1/workspace", v1WorkspaceRouter); app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use("/api/v1/membership-org", v1MembershipOrgRouter); app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use("/api/v1/membership", v1MembershipRouter); app.use('/api/v1/membership', v1MembershipRouter);
app.use("/api/v1/key", v1KeyRouter); app.use('/api/v1/key', v1KeyRouter);
app.use("/api/v1/invite-org", v1InviteOrgRouter); app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use("/api/v1/secret", v1SecretRouter); app.use('/api/v1/secret', v1SecretRouter); // deprecate
app.use("/api/v1/service-token", v1ServiceTokenRouter); // deprecated app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use("/api/v1/password", v1PasswordRouter); app.use('/api/v1/password', v1PasswordRouter);
app.use("/api/v1/stripe", v1StripeRouter); app.use('/api/v1/stripe', v1StripeRouter);
app.use("/api/v1/integration", v1IntegrationRouter); app.use('/api/v1/integration', v1IntegrationRouter);
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter); app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use("/api/v1/folder", v1SecretsFolder); app.use('/api/v1/folder', v1SecretsFolder)
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use('/api/v2/signup', v2SignupRouter);
app.use("/api/v2/auth", v2AuthRouter); app.use('/api/v2/auth', v2AuthRouter);
app.use("/api/v2/users", v2UsersRouter); app.use('/api/v2/users', v2UsersRouter);
app.use("/api/v2/organizations", v2OrganizationsRouter); app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use("/api/v2/workspace", v2EnvironmentRouter); app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use("/api/v2/workspace", v2TagsRouter); app.use('/api/v2/workspace', v2TagsRouter);
app.use("/api/v2/workspace", v2WorkspaceRouter); app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use("/api/v2/secret", v2SecretRouter); // deprecated app.use('/api/v2/secret', v2SecretRouter); // deprecate
app.use("/api/v2/secrets", v2SecretsRouter); app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use("/api/v2/service-token", v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use("/api/v2/api-key", v2APIKeyDataRouter); app.use('/api/v2/api-key', v2APIKeyDataRouter);
// v3 routes (experimental) // v3 routes (experimental)
app.use("/api/v3/auth", v3AuthRouter); app.use("/api/v3/auth", v3AuthRouter);
app.use("/api/v3/secrets", v3SecretsRouter); app.use('/api/v3/secrets', v3SecretsRouter);
app.use("/api/v3/workspaces", v3WorkspacesRouter); app.use('/api/v3/workspaces', v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter); app.use("/api/v3/signup", v3SignupRouter);
// api docs // api docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status // server status
app.use("/api", healthCheck); app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code //* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => { app.use((req, res, next) => {
if (res.headersSent) return next(); if (res.headersSent) return next();
next( next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
RouteNotFoundError({ })
message: `The requested source '(${req.method})${req.url}' was not found`,
})
);
});
app.use(requestErrorHandler); app.use(requestErrorHandler);
@@ -195,7 +166,6 @@ const main = async () => {
); );
}); });
await createTestUserForDevelopment();
setUpHealthEndpoint(server); setUpHealthEndpoint(server);
server.on("close", async () => { server.on("close", async () => {
+29 -13
View File
@@ -1559,20 +1559,35 @@ const syncSecretsGitLab = async ({
environment_scope: string; environment_scope: string;
} }
// get secrets from gitlab const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const getSecretsRes: GitLabSecret[] = ( const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`;
await standardRequest.get( const headers = {
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, "Authorization": `Bearer ${accessToken}`,
{ "Accept-Encoding": "application/json",
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 return allEnvVariables;
.filter((secret: GitLabSecret) => };
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
secret.environment_scope === integration.targetEnvironment secret.environment_scope === integration.targetEnvironment
); );
@@ -1631,6 +1646,7 @@ const syncSecretsGitLab = async ({
); );
} }
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
+41
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export * from './crypto';
+51 -50
View File
@@ -7,9 +7,6 @@ import {
getAuthAPIKeyPayload, getAuthAPIKeyPayload,
getAuthSAAKPayload getAuthSAAKPayload
} from '../helpers/auth'; } from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';
import { import {
IUser, IUser,
IServiceAccount, IServiceAccount,
@@ -45,57 +42,61 @@ const requireAuth = ({
acceptedAuthModes: string[]; acceptedAuthModes: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes] // validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue] // and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = validateAuthMode({ try {
headers: req.headers, const { authMode, authTokenValue } = validateAuthMode({
acceptedAuthModes headers: req.headers,
}); acceptedAuthModes
});
let authPayload: IUser | IServiceAccount | IServiceTokenData; let authPayload: IUser | IServiceAccount | IServiceTokenData;
switch (authMode) { switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT: case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({ authPayload = await getAuthSAAKPayload({
authTokenValue authTokenValue
}); });
req.serviceAccount = authPayload; req.serviceAccount = authPayload;
break; break;
case AUTH_MODE_SERVICE_TOKEN: case AUTH_MODE_SERVICE_TOKEN:
authPayload = await getAuthSTDPayload({ authPayload = await getAuthSTDPayload({
authTokenValue authTokenValue
}); });
req.serviceTokenData = authPayload; req.serviceTokenData = authPayload;
break; break;
case AUTH_MODE_API_KEY: case AUTH_MODE_API_KEY:
authPayload = await getAuthAPIKeyPayload({ authPayload = await getAuthAPIKeyPayload({
authTokenValue authTokenValue
}); });
req.user = authPayload; req.user = authPayload;
break; break;
default: default:
authPayload = await getAuthUserPayload({ authPayload = await getAuthUserPayload({
authTokenValue authTokenValue
}); });
req.user = authPayload; req.user = authPayload;
break; break;
}
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
}
return next();
} catch (err) {
return next(err);
} }
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
}
return next();
} }
} }
+1 -4
View File
@@ -1,9 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Bot } from '../models'; import { validateClientForBot } from '../validation';
import { validateMembership } from '../helpers/membership';
import { validateClientForBot } from '../helpers/bot';
import { AccountNotFoundError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -1,10 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Integration, IntegrationAuth } from '../models'; import { validateClientForIntegration } from '../validation';
import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership';
import { validateClientForIntegration } from '../helpers/integration';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
/** /**
* Validate if user on request is a member of workspace with proper roles associated * Validate if user on request is a member of workspace with proper roles associated
@@ -1,10 +1,6 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models'; import { validateClientForIntegrationAuth } from '../validation';
import { IntegrationService } from '../services';
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -1,13 +1,6 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors'; import { validateClientForMembership } from '../validation';
import {
Membership,
} from '../models';
import {
validateClientForMembership,
validateMembership
} from '../helpers/membership';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -1,16 +1,6 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors'; import { validateClientForMembershipOrg } from '../validation';
import {
MembershipOrg
} from '../models';
import {
validateClientForMembershipOrg,
validateMembershipOrg
} from '../helpers/membershipOrg';
// TODO: transform
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -1,9 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { IOrganization, MembershipOrg } from '../models'; import { validateClientForOrganization } from '../validation';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
import { validateMembershipOrg } from '../helpers/membershipOrg';
import { validateClientForOrganization } from '../helpers/organization';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
+1 -8
View File
@@ -1,13 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; import { validateClientForSecret } from '../validation';
import { Secret } from '../models';
import {
validateMembership
} from '../helpers/membership';
import {
validateClientForSecret
} from '../helpers/secrets';
// note: used for old /v1/secret and /v2/secret routes. // note: used for old /v1/secret and /v2/secret routes.
// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception // newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception
+1 -3
View File
@@ -1,8 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { UnauthorizedRequestError } from '../utils/errors'; import { validateClientForSecrets } from '../validation';
import { Secret, Membership } from '../models';
import { validateClientForSecrets } from '../helpers/secrets';
const requireSecretsAuth = ({ const requireSecretsAuth = ({
acceptedRoles, acceptedRoles,
@@ -1,15 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { ServiceAccount } from '../models'; import { validateClientForServiceAccount } from '../validation';
import {
ServiceAccountNotFoundError
} from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
validateClientForServiceAccount
} from '../helpers/serviceAccount';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -1,9 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { ServiceToken, ServiceTokenData } from '../models'; import { validateClientForServiceTokenData } from '../validation';
import { validateClientForServiceTokenData } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { validateClientForWorkspace } from '../helpers/workspace'; import { validateClientForWorkspace } from '../validation';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
+20
View File
@@ -1,4 +1,9 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
export interface IBackupPrivateKey { export interface IBackupPrivateKey {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -7,6 +12,8 @@ export interface IBackupPrivateKey {
iv: string; iv: string;
tag: string; tag: string;
salt: string; salt: string;
algorithm: string;
keyEncoding: 'base64' | 'utf8';
verifier: string; verifier: string;
} }
@@ -32,6 +39,19 @@ const backupPrivateKeySchema = new Schema<IBackupPrivateKey>(
select: false, select: false,
required: true 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: { salt: {
type: String, type: String,
select: false, select: false,
+23
View File
@@ -1,4 +1,10 @@
import { Schema, model, Types } from 'mongoose'; 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 { export interface IBot {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -9,6 +15,8 @@ export interface IBot {
encryptedPrivateKey: string; encryptedPrivateKey: string;
iv: string; iv: string;
tag: string; tag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'base64' | 'utf8';
} }
const botSchema = new Schema<IBot>( const botSchema = new Schema<IBot>(
@@ -45,6 +53,21 @@ const botSchema = new Schema<IBot>(
type: String, type: String,
required: true, required: true,
select: false 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
} }
}, },
{ {
+18
View File
@@ -14,6 +14,9 @@ import {
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from "../variables"; } from "../variables";
export interface IIntegrationAuth extends Document { export interface IIntegrationAuth extends Document {
@@ -31,6 +34,8 @@ export interface IIntegrationAuth extends Document {
accessCiphertext?: string; accessCiphertext?: string;
accessIV?: string; accessIV?: string;
accessTag?: string; accessTag?: string;
algorithm?: 'aes-256-gcm';
keyEncoding?: 'utf8' | 'base64';
accessExpiresAt?: Date; accessExpiresAt?: Date;
} }
@@ -109,6 +114,19 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
type: Date, type: Date,
select: false, 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, timestamps: true,
+18
View File
@@ -2,6 +2,9 @@ import { Schema, model, Types } from 'mongoose';
import { import {
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables'; } from '../variables';
import { ROOT_FOLDER_PATH } from '../utils/folder'; import { ROOT_FOLDER_PATH } from '../utils/folder';
@@ -25,6 +28,8 @@ export interface ISecret {
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'utf8' | 'base64';
tags?: string[]; tags?: string[];
path?: string; path?: string;
folder?: Types.ObjectId; folder?: Types.ObjectId;
@@ -111,6 +116,19 @@ const secretSchema = new Schema<ISecret>(
type: String, type: String,
required: false 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 // the full path to the secret in relation to folders
path: { path: {
type: String, type: String,
+24 -1
View File
@@ -1,4 +1,9 @@
import { Schema, model, Types, Document } from 'mongoose'; 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 { export interface ISecretBlindIndexData extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -6,6 +11,8 @@ export interface ISecretBlindIndexData extends Document {
encryptedSaltCiphertext: string; encryptedSaltCiphertext: string;
saltIV: string; saltIV: string;
saltTag: string; saltTag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'base64' | 'utf8'
} }
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>( const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
@@ -15,7 +22,7 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
ref: 'Workspace', ref: 'Workspace',
required: true required: true
}, },
encryptedSaltCiphertext: { encryptedSaltCiphertext: { // TODO: make these select: false
type: String, type: String,
required: true required: true
}, },
@@ -26,7 +33,23 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
saltTag: { saltTag: {
type: String, type: String,
required: true 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
} }
} }
); );
+1 -1
View File
@@ -7,9 +7,9 @@ import {
requireSecretsAuth, requireSecretsAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { validateClientForSecrets } from '../../validation';
import { query, body } from 'express-validator'; import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2'; import { secretsController } from '../../controllers/v2';
import { validateClientForSecrets } from '../../helpers/secrets';
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
-12
View File
@@ -1,4 +1,3 @@
// WIP
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
ISecret ISecret
@@ -11,7 +10,6 @@ import {
DeleteSecretParams DeleteSecretParams
} from '../interfaces/services/SecretService'; } from '../interfaces/services/SecretService';
import { import {
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper, createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper, getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper, generateSecretBlindIndexWithSaltHelper,
@@ -24,16 +22,6 @@ import {
} from '../helpers/secrets'; } from '../helpers/secrets';
class SecretService { class SecretService {
/**
*
* @param param0 h
* @returns
*/
static async initSecretBlindIndexDataHelper() {
return await initSecretBlindIndexDataHelper();
}
/** /**
* Create secret blind index data containing encrypted blind index salt * Create secret blind index data containing encrypted blind index salt
+7 -1
View File
@@ -5,6 +5,7 @@
************************************************************************************************/ ************************************************************************************************/
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models"; import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
import { SecretService } from "../services";
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { getNodeEnv } from '../config'; import { getNodeEnv } from '../config';
@@ -119,7 +120,12 @@ export const createTestUserForDevelopment = async () => {
// create workspace if not exist // create workspace if not exist
const workspaceInDB = await Workspace.findById(testWorkspaceId) const workspaceInDB = await Workspace.findById(testWorkspaceId)
if (!workspaceInDB) { 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 // create workspace key if not exist
-139
View File
@@ -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
};
+166
View File
@@ -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
};
+324
View File
@@ -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
}
}
);
}
+80
View File
@@ -0,0 +1,80 @@
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';
import { initializePassport } from '../auth';
/**
* 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());
await initializePassport();
/**
* 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();
}
+124
View File
@@ -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);
}
}
+61
View File
@@ -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'
});
}
}
+98
View File
@@ -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",
});
};
+10
View File
@@ -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';
+103
View File
@@ -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'
});
}
@@ -20,8 +20,8 @@ import {
UnauthorizedRequestError UnauthorizedRequestError
} from '../utils/errors'; } from '../utils/errors';
import { IntegrationService } from '../services'; import { IntegrationService } from '../services';
import { validateUserClientForWorkspace } from '../helpers/user'; import { validateUserClientForWorkspace } from './user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; import { validateServiceAccountClientForWorkspace } from './serviceAccount';
/** /**
* Validate authenticated clients for integration authorization with id [integrationAuthId] based * Validate authenticated clients for integration authorization with id [integrationAuthId] based
+94
View File
@@ -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'
});
}
+93
View File
@@ -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'
});
}
+104
View File
@@ -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",
});
};
+174
View File
@@ -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'
});
}
@@ -9,9 +9,9 @@ import {
IServiceTokenData, IServiceTokenData,
ISecret, ISecret,
IOrganization, IOrganization,
IServiceAccountWorkspacePermission,
ServiceAccountWorkspacePermission ServiceAccountWorkspacePermission
} from '../models'; } from '../models';
import { validateUserClientForServiceAccount } from './user';
import { import {
BadRequestError, BadRequestError,
UnauthorizedRequestError, UnauthorizedRequestError,
@@ -25,11 +25,8 @@ import {
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY AUTH_MODE_API_KEY
} from '../variables'; } from '../variables';
import {
validateUserClientForServiceAccount
} from '../helpers/user';
const validateClientForServiceAccount = async ({ export const validateClientForServiceAccount = async ({
authData, authData,
serviceAccountId, serviceAccountId,
requiredPermissions requiredPermissions
@@ -100,7 +97,7 @@ const validateClientForServiceAccount = async ({
* @param {String} environment - (optional) environment in workspace to validate against * @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint * @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/ */
const validateServiceAccountClientForWorkspace = async ({ export const validateServiceAccountClientForWorkspace = async ({
serviceAccount, serviceAccount,
workspaceId, workspaceId,
environment, environment,
@@ -169,7 +166,7 @@ const validateClientForServiceAccount = async ({
* @param {Secret[]} secrets - secrets to validate against * @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint * @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/ */
const validateServiceAccountClientForSecrets = async ({ export const validateServiceAccountClientForSecrets = async ({
serviceAccount, serviceAccount,
secrets, secrets,
requiredPermissions requiredPermissions
@@ -226,7 +223,7 @@ const validateClientForServiceAccount = async ({
* @param {ServiceAccount} targetServiceAccount - target service account to validate against * @param {ServiceAccount} targetServiceAccount - target service account to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint * @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/ */
const validateServiceAccountClientForServiceAccount = ({ export const validateServiceAccountClientForServiceAccount = ({
serviceAccount, serviceAccount,
targetServiceAccount, targetServiceAccount,
requiredPermissions requiredPermissions
@@ -248,7 +245,7 @@ const validateServiceAccountClientForServiceAccount = ({
* @param {User} obj.user - service account client * @param {User} obj.user - service account client
* @param {Organization} obj.organization - organization to validate against * @param {Organization} obj.organization - organization to validate against
*/ */
const validateServiceAccountClientForOrganization = async ({ export const validateServiceAccountClientForOrganization = async ({
serviceAccount, serviceAccount,
organization organization
}: { }: {
@@ -260,12 +257,4 @@ const validateServiceAccountClientForOrganization = async ({
message: 'Failed service account authorization for the given organization' message: 'Failed service account authorization for the given organization'
}); });
} }
}
export {
validateClientForServiceAccount,
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets,
validateServiceAccountClientForServiceAccount,
validateServiceAccountClientForOrganization
} }
@@ -18,8 +18,8 @@ import {
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY AUTH_MODE_API_KEY
} from '../variables'; } from '../variables';
import { validateUserClientForWorkspace } from '../helpers/user'; import { validateUserClientForWorkspace } from './user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; import { validateServiceAccountClientForWorkspace } from './serviceAccount';
/** /**
* Validate authenticated clients for service token with id [serviceTokenId] based * 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 {Types.ObjectId} obj.serviceTokenData - id of service token to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/ */
const validateClientForServiceTokenData = async ({ export const validateClientForServiceTokenData = async ({
authData, authData,
serviceTokenDataId, serviceTokenDataId,
acceptedRoles acceptedRoles
@@ -100,7 +100,7 @@ const validateClientForServiceTokenData = async ({
* @param {String} environment - (optional) environment in workspace to validate against * @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint * @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/ */
const validateServiceTokenDataClientForWorkspace = async ({ export const validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData, serviceTokenData,
workspaceId, workspaceId,
environment, environment,
@@ -146,7 +146,7 @@ const validateClientForServiceTokenData = async ({
* @param {Secret[]} secrets - secrets to validate against * @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint * @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/ */
const validateServiceTokenDataClientForSecrets = async ({ export const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData, serviceTokenData,
secrets, secrets,
requiredPermissions requiredPermissions
@@ -179,10 +179,4 @@ const validateClientForServiceTokenData = async ({
} }
}); });
}); });
}
export {
validateClientForServiceTokenData,
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
} }
+209
View File
@@ -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;
}
+124
View File
@@ -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'
});
}
+6 -15
View File
@@ -1,15 +1,6 @@
const ACTION_LOGIN = 'login'; export const ACTION_LOGIN = 'login';
const ACTION_LOGOUT = 'logout'; export const ACTION_LOGOUT = 'logout';
const ACTION_ADD_SECRETS = 'addSecrets'; export const ACTION_ADD_SECRETS = 'addSecrets';
const ACTION_DELETE_SECRETS = 'deleteSecrets'; export const ACTION_DELETE_SECRETS = 'deleteSecrets';
const ACTION_UPDATE_SECRETS = 'updateSecrets'; export const ACTION_UPDATE_SECRETS = 'updateSecrets';
const ACTION_READ_SECRETS = 'readSecrets'; export const ACTION_READ_SECRETS = 'readSecrets';
export {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS
}
+4 -11
View File
@@ -1,11 +1,4 @@
const AUTH_MODE_JWT = 'jwt'; export const AUTH_MODE_JWT = 'jwt';
const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount'; export const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
const AUTH_MODE_SERVICE_TOKEN = 'serviceToken'; export const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate export const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
export {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
}
+7
View File
@@ -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';
+5 -13
View File
@@ -1,14 +1,6 @@
// environments // environments
const ENV_DEV = 'dev'; export const ENV_DEV = 'dev';
const ENV_TESTING = 'test'; export const ENV_TESTING = 'test';
const ENV_STAGING = 'staging'; export const ENV_STAGING = 'staging';
const ENV_PROD = 'prod'; export const ENV_PROD = 'prod';
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]); export const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
export {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET
}
+2 -7
View File
@@ -1,7 +1,2 @@
const EVENT_PUSH_SECRETS = 'pushSecrets'; export const EVENT_PUSH_SECRETS = 'pushSecrets';
const EVENT_PULL_SECRETS = 'pullSecrets'; export const EVENT_PULL_SECRETS = 'pullSecrets';
export {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
}
+13 -154
View File
@@ -1,154 +1,13 @@
import { export * from './action';
ENV_DEV, export * from './authentication';
ENV_TESTING, export * from './crypto';
ENV_STAGING, export * from './environment';
ENV_PROD, export * from './event';
ENV_SET, export * from './integration';
} from "./environment"; export * from './organization';
import { export * from './permission';
INTEGRATION_AZURE_KEY_VAULT, export * from './secret';
INTEGRATION_AWS_PARAMETER_STORE, export * from './smtp';
INTEGRATION_AWS_SECRET_MANAGER, export * from './stripe';
INTEGRATION_HEROKU, export * from './token';
INTEGRATION_VERCEL, export * from './user';
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
};
+34 -71
View File
@@ -8,21 +8,21 @@ import {
} from '../config'; } from '../config';
// integrations // integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; export const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store'; export const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager'; export const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = "heroku"; export const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel"; export const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify"; export const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = "github"; export const INTEGRATION_GITHUB = "github";
const INTEGRATION_GITLAB = "gitlab"; export const INTEGRATION_GITLAB = "gitlab";
const INTEGRATION_RENDER = "render"; export const INTEGRATION_RENDER = "render";
const INTEGRATION_RAILWAY = "railway"; export const INTEGRATION_RAILWAY = "railway";
const INTEGRATION_FLYIO = "flyio"; export const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci"; export const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_TRAVISCI = "travisci"; export const INTEGRATION_TRAVISCI = "travisci";
const INTEGRATION_SUPABASE = 'supabase'; export const INTEGRATION_SUPABASE = 'supabase';
const INTEGRATION_SET = new Set([ export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
@@ -37,31 +37,31 @@ const INTEGRATION_SET = new Set([
]); ]);
// integration types // integration types
const INTEGRATION_OAUTH2 = "oauth2"; export const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints // integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`; export 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'; export const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL = export const INTEGRATION_VERCEL_TOKEN_URL =
"https://api.vercel.com/v2/oauth/access_token"; "https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL = export const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_token"; "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 // integration apps endpoints
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = "https://api.render.com"; export const INTEGRATION_RENDER_API_URL = "https://api.render.com";
const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2"; export const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
{ {
name: 'Heroku', name: 'Heroku',
@@ -202,41 +202,4 @@ const getIntegrationOptions = async () => {
] ]
return INTEGRATION_OPTIONS; 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
};
+5 -7
View File
@@ -1,12 +1,10 @@
// membership roles // membership roles
const OWNER = "owner"; export const OWNER = "owner";
const ADMIN = "admin"; export const ADMIN = "admin";
const MEMBER = "member"; export const MEMBER = "member";
// membership statuses // membership statuses
const INVITED = "invited"; export const INVITED = "invited";
// -- organization // -- organization
const ACCEPTED = "accepted"; export const ACCEPTED = "accepted";
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED };
+2 -7
View File
@@ -1,7 +1,2 @@
const PERMISSION_READ_SECRETS = 'read'; export const PERMISSION_READ_SECRETS = 'read';
const PERMISSION_WRITE_SECRETS = 'write'; export const PERMISSION_WRITE_SECRETS = 'write';
export {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
}
+2 -7
View File
@@ -1,8 +1,3 @@
// secrets // secrets
const SECRET_SHARED = 'shared'; export const SECRET_SHARED = 'shared';
const SECRET_PERSONAL = 'personal'; export const SECRET_PERSONAL = 'personal';
export {
SECRET_SHARED,
SECRET_PERSONAL
}
+5 -13
View File
@@ -1,13 +1,5 @@
const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net'; export const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org'; export const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com'; export const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com'; export const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
const SMTP_HOST_GMAIL = 'smtp.gmail.com'; export const SMTP_HOST_GMAIL = 'smtp.gmail.com';
export {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL
}
+2 -7
View File
@@ -1,7 +1,2 @@
const PLAN_STARTER = 'starter'; export const PLAN_STARTER = 'starter';
const PLAN_PRO = 'pro'; export const PLAN_PRO = 'pro';
export {
PLAN_STARTER,
PLAN_PRO
}
+4 -11
View File
@@ -1,11 +1,4 @@
const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation'; export const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
const TOKEN_EMAIL_MFA = 'emailMfa'; export const TOKEN_EMAIL_MFA = 'emailMfa';
const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation'; export const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset'; export const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
export {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
}
+1 -5
View File
@@ -1,5 +1 @@
const MFA_METHOD_EMAIL = 'email'; export const MFA_METHOD_EMAIL = 'email';
export {
MFA_METHOD_EMAIL
}
View File
+3 -1
View File
@@ -7,4 +7,6 @@ process.env.NODE_ENV = 'test';
process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392" process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392"
process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364" process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364"
process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2" process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2"
process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5" process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5"
process.env.ENCRYPTION_KEY="e05f54dffd58b5ab9b09e4c6fca7aff7"
process.env.ROOT_ENCRYPTION_KEY="MJA3DWJXjHiL6xjkUI2QCQuy/D+/SAbRNU1+rEo9gvQ="
+4 -2
View File
@@ -10,7 +10,9 @@ const jsrp = require('jsrp');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios'); const axios = require('axios');
import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser"; import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser";
import { encryptSymmetric } from "../../src/utils/crypto"; import {
encryptSymmetric128BitHexKeyUTF8
} from '../../src/utils/crypto';
interface TokenData { interface TokenData {
token: string; token: string;
@@ -64,7 +66,7 @@ export const getJWTFromTestUser = (): Promise<TokenData> => {
export const getServiceTokenFromTestUser = async () => { export const getServiceTokenFromTestUser = async () => {
const loggedInUserDetails = await getJWTFromTestUser() const loggedInUserDetails = await getJWTFromTestUser()
const randomBytes = crypto.randomBytes(16).toString('hex'); const randomBytes = crypto.randomBytes(16).toString('hex');
const { ciphertext, iv, tag } = encryptSymmetric({ const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: plainTextWorkspaceKey, plaintext: plainTextWorkspaceKey,
key: randomBytes, key: randomBytes,
}); });
@@ -1,9 +1,7 @@
import { describe, test, expect } from '@jest/globals'; import { describe, test, expect } from '@jest/globals';
import { import {
decryptAsymmetric, decryptAsymmetric,
decryptSymmetric,
encryptAsymmetric, encryptAsymmetric,
encryptSymmetric
} from '../../../src/utils/crypto'; } from '../../../src/utils/crypto';
describe('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');
});
});
}); });
+4 -2
View File
@@ -54,10 +54,12 @@ func GetKeyRing() (keyring.Keyring, error) {
} }
func fileKeyringPassphrasePrompt(prompt string) (string, 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 return password, nil
} else { } 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) fmt.Fprintf(os.Stderr, "%s:", prompt)
+2 -1
View File
@@ -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` To disable this hook after installing it, run the command `git config --bool hooks.infisical-scan false`
### Third party hooks management ### 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 ```bash
infisical scan git-changes --staged --verbose infisical scan git-changes --staged --verbose
@@ -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. 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. 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. 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.
## Install Infisical Helm repository ## 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. 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. 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 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`_. 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 <Info>
Each of the above environment variables can be generated by running the command `openssl rand -hex 16` in your terminal.
</Info>
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. 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 ```yaml simple-values-example.yaml
@@ -101,7 +103,7 @@ mongodb:
enabled: false 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`. Managed database connection string can be set in the `backendEnvironmentVariables`.
#### Example helm values #### Example helm values
+1 -1
View File
@@ -8,7 +8,7 @@ Please ensure you have Docker and Docker Compose installed for your OS.
- `CD` into the repo - `CD` into the repo
- run command `docker-compose -f docker-compose.dev.yml up --build --force-recreate` - 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 ### Steps to shutdown this Docker compose
@@ -137,7 +137,7 @@ const attemptLogin = async (
} }
if (email) { if (email) {
telemetry.identify(email); telemetry.identify(email, email);
telemetry.capture('User Logged In'); telemetry.capture('User Logged In');
} }
@@ -22,10 +22,12 @@ class Capturer {
} }
} }
identify(id: string) { identify(id: string, email?: string) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) { if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
try { try {
this.api.identify(id); this.api.identify(id, {
email: email
});
} catch (error) { } catch (error) {
console.error('PostHog', error); console.error('PostHog', error);
} }
+1 -1
View File
@@ -10,7 +10,7 @@ export type CardTitleProps = {
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => ( export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div <div
className={twMerge( className={twMerge(
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', 'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600 break-words',
className className
)} )}
> >
@@ -76,7 +76,7 @@ export const DeleteActionModal = ({
<form> <form>
<FormControl <FormControl
label={ label={
<div className="pb-2 text-sm"> <div className="pb-2 text-sm break-words">
Type <span className="font-bold">{deleteKey}</span> to delete the resource Type <span className="font-bold">{deleteKey}</span> to delete the resource
</div> </div>
} }
+7 -3
View File
@@ -1,6 +1,6 @@
import { forwardRef, ReactNode } from 'react'; import { forwardRef, ReactNode } from 'react';
import { IconProp } from '@fortawesome/fontawesome-svg-core'; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as SelectPrimitive from '@radix-ui/react-select'; import * as SelectPrimitive from '@radix-ui/react-select';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
@@ -63,7 +63,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
style={{ width: 'var(--radix-select-trigger-width)' }} style={{ width: 'var(--radix-select-trigger-width)' }}
> >
<SelectPrimitive.ScrollUpButton> <SelectPrimitive.ScrollUpButton>
<FontAwesomeIcon icon={faChevronUp} size="sm" /> <div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretUp} size="sm" />
</div>
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1"> <SelectPrimitive.Viewport className="p-1">
{isLoading ? ( {isLoading ? (
@@ -76,7 +78,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
)} )}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton> <SelectPrimitive.ScrollDownButton>
<FontAwesomeIcon icon={faCaretDown} size="xs" /> <div className="flex items-center justify-center">
<FontAwesomeIcon icon={faCaretDown} size="sm" />
</div>
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
+5 -3
View File
@@ -261,9 +261,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
router.push(`/dashboard/${value}`); router.push(`/dashboard/${value}`);
}} }}
position="popper" 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 <div className='h-full no-scrollbar no-scrollbar::-webkit-scrollbar'>
{workspaces
.filter((ws) => ws.organization === currentOrg?._id) .filter((ws) => ws.organization === currentOrg?._id)
.map(({ _id, name }) => ( .map(({ _id, name }) => (
<SelectItem <SelectItem
@@ -274,7 +275,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
{name} {name}
</SelectItem> </SelectItem>
))} ))}
{/* <hr className="mt-1 mb-1 h-px border-0 bg-gray-700" /> */} </div>
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full"> <div className="w-full">
<Button <Button
className="w-full bg-mineshaft-700 py-2 text-bunker-200" className="w-full bg-mineshaft-700 py-2 text-bunker-200"