Merge branch 'main' into feature/google-signin-signup-integration

This commit is contained in:
Sheen Capadngan
2023-05-28 22:12:02 +08:00
97 changed files with 2555 additions and 2090 deletions
+7 -9
View File
@@ -33,7 +33,7 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.1.3",
"infisical-node": "^1.2.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
@@ -6808,13 +6808,12 @@
}
},
"node_modules/infisical-node": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz",
"integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.2.1.tgz",
"integrity": "sha512-zEB0w5+1O0mv9qc68bq4f9jDjrtwdbqjJebnwodgy8U1XZElDXeMDQgSMCtgYan7JRmVlH6s/LM8X7kUF+67ZA==",
"dependencies": {
"axios": "^1.3.3",
"dotenv": "^16.0.3",
"node-cache": "^5.1.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
}
@@ -18432,13 +18431,12 @@
"dev": true
},
"infisical-node": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz",
"integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.2.1.tgz",
"integrity": "sha512-zEB0w5+1O0mv9qc68bq4f9jDjrtwdbqjJebnwodgy8U1XZElDXeMDQgSMCtgYan7JRmVlH6s/LM8X7kUF+67ZA==",
"requires": {
"axios": "^1.3.3",
"dotenv": "^16.0.3",
"node-cache": "^5.1.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
}
+1 -1
View File
@@ -24,7 +24,7 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.1.3",
"infisical-node": "^1.2.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
+17 -4
View File
@@ -1,12 +1,19 @@
import InfisicalClient from 'infisical-node';
const client = new InfisicalClient({
export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!
});
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
export const getEncryptionKey = async () => {
const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true'
export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue;
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
@@ -50,8 +57,14 @@ export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWOR
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
export const getLicenseKey = async () => (await client.getSecret('LICENSE_KEY')).secretValue;
export const getLicenseServerKey = async () => (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
// TODO: deprecate from here
@@ -5,7 +5,7 @@ import {
IntegrationAuth,
Bot
} from '../../models';
import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
import { IntegrationService } from '../../services';
import {
getApps,
@@ -129,7 +129,9 @@ export const saveIntegrationAccessToken = async (
integration
}, {
workspace: new Types.ObjectId(workspaceId),
integration
integration,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true,
upsert: true
@@ -39,7 +39,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
error: 'Failed to send email verification code'
});
}
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});
@@ -6,7 +6,7 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre
const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { TelemetryService } from '../../services';
import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors';
@@ -36,7 +36,9 @@ export const createSecret = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
environment,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id)
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
@@ -92,7 +94,9 @@ export const createSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
environment,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id)
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
sanitizedSecretesToCreate.push(safeUpdateFields)
+50 -21
View File
@@ -7,7 +7,9 @@ import {
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
ACTION_DELETE_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../../variables';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors';
import { EventService } from '../../services';
@@ -51,7 +53,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
const isPaid = orgPlan.tier >= 1;
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
@@ -85,7 +87,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
path: fullFolderPath,
folder: folderId,
secretBlindIndex
secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
break;
case 'PATCH':
@@ -100,6 +104,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
secretBlindIndex,
folder: folderId,
path: fullFolderPath,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
break;
case 'DELETE':
@@ -144,7 +150,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId,
channel,
userAgent: req.headers?.['user-agent'],
isPaid
isPaid,
email: req.user.email
}
});
}
@@ -201,6 +208,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: u.secretCommentCiphertext,
secretCommentIV: u.secretCommentIV,
secretCommentTag: u.secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags
}));
@@ -232,7 +241,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId,
channel,
userAgent: req.headers?.['user-agent'],
isPaid
isPaid,
email: req.user.email
}
});
}
@@ -268,7 +278,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId,
channel: channel,
userAgent: req.headers?.['user-agent'],
isPaid
isPaid,
email: req.user.email
}
});
}
@@ -387,7 +398,7 @@ export const createSecrets = async (req: Request, res: Response) => {
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
const isPaid = orgPlan.tier >= 1;
let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) {
@@ -457,6 +468,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags
});
})
@@ -503,7 +516,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}))
});
@@ -545,7 +560,10 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId,
channel: channel,
userAgent: req.headers?.['user-agent'],
isPaid
isPaid,
email: await TelemetryService.getDistinctId({
authData: req.authData
})
}
});
}
@@ -613,7 +631,7 @@ export const getSecrets = async (req: Request, res: Response) => {
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
const isPaid = orgPlan.tier >= 1;
// secrets to return
let secrets: ISecret[] = [];
@@ -748,7 +766,10 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId,
channel,
userAgent: req.headers?.['user-agent'],
isPaid
isPaid,
email: await TelemetryService.getDistinctId({
authData: req.authData
})
}
});
}
@@ -852,6 +873,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
...((
secretCommentCiphertext !== undefined &&
@@ -905,6 +928,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags: tags ? tags : secret.tags
});
})
@@ -963,7 +988,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
const isPaid = orgPlan.tier >= 1;
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
@@ -978,7 +1003,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: key,
channel: channel,
userAgent: req.headers?.['user-agent'],
isPaid
isPaid,
email: await TelemetryService.getDistinctId({
authData: req.authData
})
}
});
}
@@ -1100,13 +1128,11 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key)
});
const organizationId = (
await Workspace.findOne({
_id: key
})
)?.organization?.toString();
const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || '');
const isPaid = orgPlan.slug != 'starter';
const workspace = await Workspace.findById(key);
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier >= 1;
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
@@ -1121,7 +1147,10 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key,
channel: channel,
userAgent: req.headers?.['user-agent'],
isPaid
isPaid,
email: await TelemetryService.getDistinctId({
authData: req.authData
})
}
});
}
@@ -7,13 +7,12 @@ import { EELicenseService } from '../../services';
* Return the organization's current plan and allowed feature set
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
const plan = await EELicenseService.getOrganizationPlan(req.organization._id.toString());
const { organizationId } = req.params;
// cache fetched plan for organization
EELicenseService.localFeatureSet.set(req.organization._id.toString(), plan);
const plan = await EELicenseService.getOrganizationPlan(organizationId);
return res.status(200).send({
plan
plan,
});
}
@@ -162,6 +162,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding
} = oldSecretVersion;
// update secret
@@ -182,6 +184,8 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding
},
{
new: true
@@ -205,7 +209,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
algorithm,
keyEncoding
}).save();
// take secret snapshot
+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 {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper,
markDeletedSecretVersionsHelper
};
+19 -1
View File
@@ -2,6 +2,9 @@ import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../../variables';
export interface ISecretVersion {
@@ -20,6 +23,8 @@ export interface ISecretVersion {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'utf8' | 'base64';
}
const secretVersionSchema = new Schema<ISecretVersion>(
@@ -85,7 +90,20 @@ const secretVersionSchema = new Schema<ISecretVersion>(
secretValueTag: {
type: String, // symmetric
required: true
}
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
},
},
{
timestamps: true
+15 -6
View File
@@ -17,9 +17,11 @@ import { OrganizationNotFoundError } from '../../utils/errors';
interface FeatureSet {
_id: string | null;
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
tier: number | null;
projectLimit: number | null;
tier: number;
workspaceLimit: number | null;
workspacesUsed: number;
memberLimit: number | null;
membersUsed: number;
secretVersioning: boolean;
pitRecovery: boolean;
rbac: boolean;
@@ -44,8 +46,10 @@ class EELicenseService {
_id: null,
slug: null,
tier: -1,
projectLimit: null,
workspaceLimit: null,
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
secretVersioning: true,
pitRecovery: true,
rbac: true,
@@ -63,11 +67,13 @@ class EELicenseService {
});
}
public async getOrganizationPlan(organizationId: string) {
public async getOrganizationPlan(organizationId: string): Promise<FeatureSet> {
try {
if (this.instanceType === 'cloud') {
const cachedPlan = this.localFeatureSet.get(organizationId);
if (cachedPlan) return cachedPlan;
const cachedPlan = this.localFeatureSet.get<FeatureSet>(organizationId);
if (cachedPlan) {
return cachedPlan;
}
const organization = await Organization.findById(organizationId);
if (!organization) throw OrganizationNotFoundError();
@@ -76,6 +82,9 @@ class EELicenseService {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
);
// cache fetched plan for organization
this.localFeatureSet.set(organizationId, currentPlan);
return currentPlan;
}
} catch (err) {
+1 -11
View File
@@ -3,8 +3,7 @@ import { ISecretVersion } from '../models';
import {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
markDeletedSecretVersionsHelper
} from '../helpers/secret';
import EELicenseService from './EELicenseService';
@@ -64,15 +63,6 @@ class EESecretService {
secretIds
});
}
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
static async initSecretVersioning() {
if (!EELicenseService.isLicenseValid) return;
await initSecretVersioningHelper();
}
}
export default EESecretService;
-1
View File
@@ -42,7 +42,6 @@ const validateAuthMode = ({
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[]
}) => {
// TODO: refactor middleware
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
+98 -131
View File
@@ -4,107 +4,26 @@ import {
BotKey,
Secret,
ISecret,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
IUser
} from "../models";
import {
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric,
} from "../utils/crypto";
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric
} from '../utils/crypto';
import {
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from "../variables";
import { getEncryptionKey } from "../config";
import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors";
import { validateMembership } from "../helpers/membership";
import { validateUserClientForWorkspace } from "../helpers/user";
import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount";
/**
* Validate authenticated clients for bot with id [botId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.botId - id of bot to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForBot = async ({
authData,
botId,
acceptedRoles,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
botId: Types.ObjectId;
acceptedRoles: Array<"admin" | "member">;
}) => {
const bot = await Bot.findById(botId);
if (!bot) throw BotNotFoundError();
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: bot.workspace,
});
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for bot",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
return bot;
}
throw BotNotFoundError({
message: "Failed client authorization for bot",
});
};
import {
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { InternalServerError } from "../utils/errors";
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -119,23 +38,52 @@ const createBot = async ({
name: string;
workspaceId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey,
key: await getEncryptionKey(),
if (rootEncryptionKey) {
const {
ciphertext,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: await getEncryptionKey(),
});
return await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: 'Failed to create new bot due to missing encryption key'
});
const bot = await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
}).save();
return bot;
};
/**
@@ -161,14 +109,14 @@ const getSecretsHelper = async ({
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
});
const secretValue = decryptSymmetric({
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
@@ -189,34 +137,54 @@ const getSecretsHelper = async ({
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId,
}).populate<{ sender: IUser }>("sender", "publicKey");
})
.populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({
workspace: workspaceId,
}).select("+encryptedPrivateKey +iv +tag");
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
if (!bot) throw new Error("Failed to find bot");
if (!bot.isActive) throw new Error("Bot is not active");
const privateKeyBot = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: await getEncryptionKey(),
});
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
// case: encoding scheme is base64
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
const key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
}
return key;
throw InternalServerError({
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
});
};
/**
@@ -234,7 +202,7 @@ const encryptSymmetricHelper = async ({
plaintext: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const { ciphertext, iv, tag } = encryptSymmetric({
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext,
key,
});
@@ -266,7 +234,7 @@ const decryptSymmetricHelper = async ({
tag: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const plaintext = decryptSymmetric({
const plaintext = decryptSymmetric128BitHexKeyUTF8({
ciphertext,
iv,
tag,
@@ -277,9 +245,8 @@ const decryptSymmetricHelper = async ({
};
export {
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper,
decryptSymmetricHelper
};
+1 -5
View File
@@ -1,6 +1,4 @@
import mongoose from 'mongoose';
import { EESecretService } from '../ee/services';
import { SecretService } from '../services';
import { getLogger } from '../utils/logger';
/**
@@ -21,9 +19,7 @@ const initDatabaseHelper = async ({
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
(await getLogger("database")).info("Database connection established");
await EESecretService.initSecretVersioning();
await SecretService.initSecretBlindIndexDataHelper();
} catch (err) {
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
}
+10 -105
View File
@@ -3,40 +3,20 @@ import { Types } from 'mongoose';
import {
Bot,
Integration,
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
IntegrationAuth
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables';
import {
UnauthorizedRequestError,
IntegrationAuthNotFoundError,
IntegrationNotFoundError
} from '../utils/errors';
import RequestError from '../utils/requestError';
import {
validateClientForIntegrationAuth
} from '../helpers/integrationAuth';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import { IntegrationService } from '../services';
interface Update {
workspace: string;
@@ -45,84 +25,6 @@ interface Update {
accountId?: string;
}
/**
* Validate authenticated clients for integration with id [integrationId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.integrationId - id of integration to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForIntegration = async ({
authData,
integrationId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
integrationId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const integration = await Integration.findById(integrationId);
if (!integration) throw IntegrationNotFoundError();
const integrationAuth = await IntegrationAuth
.findById(integration.integrationAuth)
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) throw IntegrationAuthNotFoundError();
const accessToken = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id
})).accessToken;
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integration.workspace,
acceptedRoles
});
return ({ integration, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: integration.workspace
});
return ({ integration, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for integration'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integration.workspace,
acceptedRoles
});
return ({ integration, accessToken });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for integration'
});
}
/**
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
* named [integration]
@@ -400,7 +302,9 @@ const setIntegrationAuthRefreshHelper = async ({
}, {
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
@@ -461,7 +365,9 @@ const setIntegrationAuthAccessHelper = async ({
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
@@ -475,7 +381,6 @@ const setIntegrationAuthAccessHelper = async ({
}
export {
validateClientForIntegration,
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
+2 -96
View File
@@ -2,105 +2,12 @@ import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
Key
} from '../models';
import {
MembershipNotFoundError,
BadRequestError,
UnauthorizedRequestError
BadRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
/**
* Validate authenticated clients for membership with id [membershipId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
* @returns {Membership} - validated membership
*/
const validateClientForMembership = async ({
authData,
membershipId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const membership = await Membership.findById(membershipId);
if (!membership) throw MembershipNotFoundError({
message: 'Failed to find membership'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: membership.workspace
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: new Types.ObjectId(membership.workspace)
});
return membership;
}
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for membership'
});
}
/**
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
@@ -230,7 +137,6 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
};
export {
validateClientForMembership,
validateMembership,
addMemberships,
findMembership,
+1 -85
View File
@@ -3,95 +3,12 @@ import {
MembershipOrg,
Workspace,
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
Key
} from '../models';
import {
MembershipOrgNotFoundError,
BadRequestError,
UnauthorizedRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate authenticated clients for organization membership with id [membershipOrgId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against
* @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles
* @param {MembershipOrg} - validated organization membership
*/
const validateClientForMembershipOrg = async ({
authData,
membershipOrgId,
acceptedRoles,
acceptedStatuses
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipOrgId: Types.ObjectId;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
if (!membershipOrg) throw MembershipOrgNotFoundError({
message: 'Failed to find organization membership '
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for organization membership'
});
}
/**
* Validate that user with id [userId] is a member of organization with id [organizationId]
@@ -234,7 +151,6 @@ const deleteMembershipOrg = async ({
};
export {
validateClientForMembershipOrg,
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,
+4 -104
View File
@@ -1,21 +1,8 @@
import Stripe from "stripe";
import { Types } from "mongoose";
import {
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
} from "../models";
import { Organization, MembershipOrg } from "../models";
import {
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER,
ACCEPTED
} from "../variables";
import {
getStripeSecretKey,
@@ -23,12 +10,6 @@ import {
getStripeProductTeam,
getStripeProductStarter,
} from "../config";
import {
UnauthorizedRequestError,
OrganizationNotFoundError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "../helpers/user";
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
import {
EELicenseService
} from '../ee/services';
@@ -40,88 +21,6 @@ import {
licenseKeyRequest
} from '../config/request';
/**
* Validate accepted clients for organization with id [organizationId]
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
*/
const validateClientForOrganization = async ({
authData,
organizationId,
acceptedRoles,
acceptedStatuses,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
organizationId: Types.ObjectId;
acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<"invited" | "accepted">;
}) => {
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization",
});
}
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
return { organization, membershipOrg };
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForOrganization({
serviceAccount: authData.authPayload,
organization,
});
return { organization };
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for organization",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
return { organization, membershipOrg };
}
throw UnauthorizedRequestError({
message: "Failed client authorization for organization",
});
};
/**
* Create an organization with name [name]
* @param {Object} obj
@@ -251,6 +150,8 @@ const updateSubscriptionOrgQuantity = async ({
quantity
}
);
EELicenseService.localFeatureSet.del(organizationId);
}
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
@@ -273,8 +174,7 @@ const updateSubscriptionOrgQuantity = async ({
};
export {
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity,
updateSubscriptionOrgQuantity
};
+14
View File
@@ -9,6 +9,8 @@ import {
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
} from "../variables";
import _ from "lodash";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
@@ -194,6 +196,8 @@ const v1PushSecrets = async ({
secretValueIV: newSecret.ivValue,
secretValueTag: newSecret.tagValue,
secretValueHash: newSecret.hashValue,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
}),
});
@@ -225,6 +229,8 @@ const v1PushSecrets = async ({
secretCommentIV: s.ivComment,
secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
};
if (toAdd[idx].type === "personal") {
@@ -254,6 +260,8 @@ const v1PushSecrets = async ({
secretValueIV,
secretValueTag,
secretValueHash,
algorithm,
keyEncoding
}) =>
new SecretVersion({
secret: _id,
@@ -271,6 +279,8 @@ const v1PushSecrets = async ({
secretValueIV,
secretValueTag,
secretValueHash,
algorithm,
keyEncoding
})
),
});
@@ -467,6 +477,8 @@ const v2PushSecrets = async ({
workspace: workspaceId,
type: toAdd[idx].type,
environment,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
...(toAdd[idx].type === "personal" ? { user: userId } : {}),
}))
);
@@ -478,6 +490,8 @@ const v2PushSecrets = async ({
...secretDocument,
secret: secretDocument._id,
isDeleted: false,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
}),
});
+84 -262
View File
@@ -7,58 +7,35 @@ import {
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
AuthData
} from '../interfaces/middleware';
import {
User,
Workspace,
ServiceAccount,
ServiceTokenData,
Secret,
ISecret,
SecretBlindIndexData,
} from '../models';
import { SecretVersion } from '../ee/models';
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForSecret,
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets,
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets,
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError,
SecretBlindIndexDataNotFoundError
SecretBlindIndexDataNotFoundError,
InternalServerError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
SECRET_PERSONAL,
SECRET_SHARED,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
ACTION_DELETE_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
import crypto from 'crypto';
import * as argon2 from 'argon2';
import {
encryptSymmetric,
decryptSymmetric
import {
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8
} from '../utils/crypto';
import { getEncryptionKey } from '../config';
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
import { TelemetryService } from '../services';
import {
EESecretService,
@@ -69,199 +46,6 @@ import {
getAuthDataPayloadUserObj
} from '../utils/auth';
/**
* Validate authenticated clients for secrets with id [secretId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecret = async ({
authData,
secretId,
acceptedRoles,
requiredPermissions
}: {
authData: AuthData;
secretId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => {
const secret = await Secret.findById(secretId);
if (!secret) throw SecretNotFoundError({
message: 'Failed to find secret'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment
});
return secret;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secret'
});
}
/**
* Validate authenticated clients for secrets with ids [secretIds] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecrets = async ({
authData,
secretIds,
requiredPermissions
}: {
authData: AuthData;
secretIds: Types.ObjectId[];
requiredPermissions: string[];
}) => {
let secrets: ISecret[] = [];
secrets = await Secret.find({
_id: {
$in: secretIds
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForSecrets({
serviceTokenData: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secrets resource'
});
}
/**
* Initialize secret blind index data by setting previously
* un-initialized projects to have secret blind index data
* (Ensures that all projects have associated blind index data)
*/
const initSecretBlindIndexDataHelper = async () => {
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
_id: {
$nin: workspaceIdsBlindIndexed
}
});
const secretBlindIndexDataToInsert = await Promise.all(
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric({
plaintext: salt,
key: await getEncryptionKey()
});
const secretBlindIndexData = new SecretBlindIndexData({
workspace: workspaceToBlindIndex,
encryptedSaltCiphertext,
saltIV,
saltTag
})
return secretBlindIndexData;
})
);
if (secretBlindIndexDataToInsert.length > 0) {
await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
}
}
/**
* Create secret blind index data containing encrypted blind index [salt]
* for workspace with id [workspaceId]
@@ -273,26 +57,47 @@ const createSecretBlindIndexDataHelper = async ({
}: {
workspaceId: Types.ObjectId;
}) => {
// initialize random blind index salt for workspace
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric({
plaintext: salt,
key: await getEncryptionKey()
});
const secretBlindIndexData = await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag
}).save();
return secretBlindIndexData;
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (rootEncryptionKey) {
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey);
return await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else {
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt,
key: encryptionKey
});
return await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
}
/**
@@ -306,22 +111,36 @@ const getSecretBlindIndexSaltHelper = async ({
}: {
workspaceId: Types.ObjectId;
}) => {
// check if workspace blind index data exists
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId
});
}).select('+algorithm +keyEncoding');
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt
const salt = decryptSymmetric({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: await getEncryptionKey()
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
return client.decryptSymmetric(
secretBlindIndexData.encryptedSaltCiphertext,
rootEncryptionKey,
secretBlindIndexData.saltIV,
secretBlindIndexData.saltTag
);
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
// decrypt workspace salt
return decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: encryptionKey
});
}
throw InternalServerError({
message: 'Failed to obtain workspace salt needed for secret blind indexing'
});
return salt;
}
/**
@@ -376,7 +195,7 @@ const generateSecretBlindIndexHelper = async ({
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt
const salt = decryptSymmetric({
const salt = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
@@ -464,7 +283,9 @@ const createSecretHelper = async ({
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
const secretVersion = new SecretVersion({
@@ -481,7 +302,9 @@ const createSecretHelper = async ({
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
// // (EE) add version for new secret
@@ -771,7 +594,9 @@ const updateSecretHelper = async ({
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
// (EE) add version for new secret
@@ -932,9 +757,6 @@ const deleteSecretHelper = async ({
}
export {
validateClientForSecret,
validateClientForSecrets,
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
+4 -222
View File
@@ -1,24 +1,8 @@
import { Types } from 'mongoose';
import {
IUser,
ISecret,
IServiceAccount,
User,
Membership,
IOrganization,
Organization,
} from '../models';
import { sendMail } from './nodemailer';
import { validateMembership } from './membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../variables';
/**
* Initialize a user under email [email]
@@ -26,7 +10,7 @@ import {
* @param {String} obj.email - email of user to initialize
* @returns {Object} user - the initialized user
*/
const setupAccount = async ({ email }: { email: string }) => {
export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({
email
}).save();
@@ -52,7 +36,7 @@ const setupAccount = async ({ email }: { email: string }) => {
* @param {String} obj.verifier - verifier for auth SRP
* @returns {Object} user - the completed user
*/
const completeAccount = async ({
export const completeAccount = async ({
userId,
firstName,
lastName,
@@ -113,7 +97,7 @@ const completeAccount = async ({
* @param {String} obj.ip - login ip address
* @param {String} obj.userAgent - login user-agent
*/
const checkUserDevice = async ({
export const checkUserDevice = async ({
user,
ip,
userAgent
@@ -148,206 +132,4 @@ const checkUserDevice = async ({
}
});
}
}
/**
* Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForWorkspace = async ({
user,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
// validate user membership in workspace
const membership = await validateMembership({
userId: user._id,
workspaceId,
acceptedRoles
});
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
break;
case PERMISSION_WRITE_SECRETS:
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
return membership;
}
/**
* Validate that user (client) can access secret [secret]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecret = async ({
user,
secret,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
secret: ISecret;
acceptedRoles?: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
const membership = await validateMembership({
userId: user._id,
workspaceId: secret.workspace,
acceptedRoles
});
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
}
}
/**
* Validate that user (client) can access secrets [secrets]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecrets = async ({
user,
secrets,
requiredPermissions
}: {
user: IUser;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
// TODO: add acceptedRoles?
const userMemberships = await Membership.find({ user: user._id })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (!workspaceIdsSet.has(secret.workspace.toString())) {
throw BadRequestError({
message: 'Failed authorization for the secret'
});
}
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
}
});
}
/**
* Validate that user (client) can access service account [serviceAccount]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForServiceAccount = async ({
user,
serviceAccount,
requiredPermissions
}: {
user: IUser;
serviceAccount: IServiceAccount;
requiredPermissions?: string[];
}) => {
if (!serviceAccount.user.equals(user._id)) {
// case: user who created service account is not the
// same user that is on the request
await validateMembershipOrg({
userId: user._id,
organizationId: serviceAccount.organization,
acceptedRoles: [],
acceptedStatuses: []
});
}
}
/**
* Validate that user (client) can access organization [organization]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Organization} obj.organization - organization to validate against
*/
const validateUserClientForOrganization = async ({
user,
organization,
acceptedRoles,
acceptedStatuses
}: {
user: IUser;
organization: IOrganization;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await validateMembershipOrg({
userId: user._id,
organizationId: organization._id,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
export {
setupAccount,
completeAccount,
checkUserDevice,
validateUserClientForWorkspace,
validateUserClientForSecrets,
validateUserClientForServiceAccount,
validateUserClientForOrganization,
validateUserClientForSecret
};
}
+1 -124
View File
@@ -1,136 +1,14 @@
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { Types } from 'mongoose';
import {
Workspace,
Bot,
Membership,
Key,
Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
SecretBlindIndexData
Secret
} from '../models';
import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { encryptSymmetric } from '../utils/crypto';
import { SecretService } from '../services';
/**
* Validate authenticated clients for workspace with id [workspaceId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForWorkspace = async ({
authData,
workspaceId,
environment,
acceptedRoles,
requiredPermissions,
requireBlindIndicesEnabled
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: 'Failed to find workspace'
});
if (requireBlindIndicesEnabled) {
// case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: 'Failed workspace authorization due to blind indices not being enabled'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership, workspace });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership, workspace });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for workspace'
});
}
/**
* Create a workspace with name [name] in organization with id [organizationId]
* and a bot for it.
@@ -203,7 +81,6 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
};
export {
validateClientForWorkspace,
createWorkspace,
deleteWorkspace
};
+75 -104
View File
@@ -1,34 +1,26 @@
import dotenv from "dotenv";
dotenv.config();
import express from "express";
import helmet from "helmet";
import cors from "cors";
import * as Sentry from "@sentry/node";
import { DatabaseService } from "./services";
import { EELicenseService } from "./ee/services";
import { setUpHealthEndpoint } from "./services/health";
import { initSmtp } from "./services/smtp";
import { TelemetryService } from "./services";
import { setTransporter } from "./helpers/nodemailer";
import { createTestUserForDevelopment } from "./utils/addDevelopmentUser";
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { DatabaseService } from './services';
import { EELicenseService } from './ee/services';
import { setUpHealthEndpoint } from './services/health';
import cookieParser from 'cookie-parser';
import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require("./utils/patchAsyncRoutes");
import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express");
const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require("request-ip");
import { apiLimiter } from "./helpers/rateLimiter";
const requestIp = require('request-ip');
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter,
organizations as eeOrganizationsRouter,
cloudProducts as eeCloudProductsRouter,
} from "./ee/routes/v1";
cloudProducts as eeCloudProductsRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
@@ -47,8 +39,8 @@ import {
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter,
secretsFolder as v1SecretsFolder,
} from "./routes/v1";
secretsFolder as v1SecretsFolder
} from './routes/v1';
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
@@ -62,130 +54,109 @@ import {
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from "./routes/v2";
} from './routes/v2';
import {
auth as v3AuthRouter,
secrets as v3SecretsRouter,
signup as v3SignupRouter,
workspaces as v3WorkspacesRouter,
} from "./routes/v3";
import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler";
workspaces as v3WorkspacesRouter
} from './routes/v3';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
import {
getMongoURL,
getNodeEnv,
getPort,
getSentryDSN,
getSiteURL,
} from "./config";
import { initializePassport } from "./utils/auth";
getSiteURL
} from './config';
import { setup } from './utils/setup';
const main = async () => {
TelemetryService.logTelemetryMessage();
setTransporter(await initSmtp());
await setup();
await EELicenseService.initGlobalFeatureSet();
await DatabaseService.initDatabase(await getMongoURL());
if ((await getNodeEnv()) !== "test") {
Sentry.init({
dsn: await getSentryDSN(),
tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === "production" ? false : true,
environment: await getNodeEnv(),
});
}
patchRouterParam();
const app = express();
await initializePassport();
app.enable("trust proxy");
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: await getSiteURL(),
origin: await getSiteURL()
})
);
app.use(requestIp.mw());
if ((await getNodeEnv()) === "production") {
if ((await getNodeEnv()) === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable("x-powered-by");
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// (EE) routes
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use("/api/v1/workspace", eeWorkspaceRouter);
app.use("/api/v1/action", eeActionRouter);
app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
app.use('/api/v1/organizations', eeOrganizationsRouter);
app.use('/api/v1/cloud-products', eeCloudProductsRouter);
// v1 routes (default)
app.use("/api/v1/signup", v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter);
app.use("/api/v1/bot", v1BotRouter);
app.use("/api/v1/user", v1UserRouter);
app.use("/api/v1/user-action", v1UserActionRouter);
app.use("/api/v1/organization", v1OrganizationRouter);
app.use("/api/v1/workspace", v1WorkspaceRouter);
app.use("/api/v1/membership-org", v1MembershipOrgRouter);
app.use("/api/v1/membership", v1MembershipRouter);
app.use("/api/v1/key", v1KeyRouter);
app.use("/api/v1/invite-org", v1InviteOrgRouter);
app.use("/api/v1/secret", v1SecretRouter);
app.use("/api/v1/service-token", v1ServiceTokenRouter); // deprecated
app.use("/api/v1/password", v1PasswordRouter);
app.use("/api/v1/stripe", v1StripeRouter);
app.use("/api/v1/integration", v1IntegrationRouter);
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
app.use("/api/v1/folder", v1SecretsFolder);
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter); // deprecate
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v1/folder', v1SecretsFolder)
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);
app.use("/api/v2/auth", v2AuthRouter);
app.use("/api/v2/users", v2UsersRouter);
app.use("/api/v2/organizations", v2OrganizationsRouter);
app.use("/api/v2/workspace", v2EnvironmentRouter);
app.use("/api/v2/workspace", v2TagsRouter);
app.use("/api/v2/workspace", v2WorkspaceRouter);
app.use("/api/v2/secret", v2SecretRouter); // deprecated
app.use("/api/v2/secrets", v2SecretsRouter);
app.use("/api/v2/service-token", v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
app.use("/api/v2/api-key", v2APIKeyDataRouter);
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecate
app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// v3 routes (experimental)
app.use("/api/v3/auth", v3AuthRouter);
app.use("/api/v3/secrets", v3SecretsRouter);
app.use("/api/v3/workspaces", v3WorkspacesRouter);
app.use('/api/v3/secrets', v3SecretsRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter);
// api docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status
app.use("/api", healthCheck);
// server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(
RouteNotFoundError({
message: `The requested source '(${req.method})${req.url}' was not found`,
})
);
});
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
app.use(requestErrorHandler);
@@ -195,7 +166,7 @@ const main = async () => {
);
});
await createTestUserForDevelopment();
// await createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on("close", async () => {
+29 -13
View File
@@ -1559,20 +1559,35 @@ const syncSecretsGitLab = async ({
environment_scope: string;
}
// get secrets from gitlab
const getSecretsRes: GitLabSecret[] = (
await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`;
const headers = {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
};
let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}?per_page=100`;
while (url) {
const response: any = await standardRequest.get(url, { headers });
allEnvVariables = [...allEnvVariables, ...response.data];
const linkHeader = response.headers.link;
const nextLink = linkHeader?.split(',').find((part: string) => part.includes('rel="next"'));
if (nextLink) {
url = nextLink.trim().split(';')[0].slice(1, -1);
} else {
url = null;
}
)
)
.data
.filter((secret: GitLabSecret) =>
}
return allEnvVariables;
};
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
secret.environment_scope === integration.targetEnvironment
);
@@ -1631,6 +1646,7 @@ const syncSecretsGitLab = async ({
);
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
+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';
+1 -3
View File
@@ -7,9 +7,6 @@ import {
getAuthAPIKeyPayload,
getAuthSAAKPayload
} from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';
import {
IUser,
IServiceAccount,
@@ -48,6 +45,7 @@ const requireAuth = ({
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
+1 -4
View File
@@ -1,9 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Bot } from '../models';
import { validateMembership } from '../helpers/membership';
import { validateClientForBot } from '../helpers/bot';
import { AccountNotFoundError } from '../utils/errors';
import { validateClientForBot } from '../validation';
type req = 'params' | 'body' | 'query';
@@ -1,10 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Integration, IntegrationAuth } from '../models';
import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership';
import { validateClientForIntegration } from '../helpers/integration';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
import { validateClientForIntegration } from '../validation';
/**
* Validate if user on request is a member of workspace with proper roles associated
@@ -1,10 +1,6 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models';
import { IntegrationService } from '../services';
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
import { validateClientForIntegrationAuth } from '../validation';
type req = 'params' | 'body' | 'query';
@@ -1,13 +1,6 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
Membership,
} from '../models';
import {
validateClientForMembership,
validateMembership
} from '../helpers/membership';
import { validateClientForMembership } from '../validation';
type req = 'params' | 'body' | 'query';
@@ -1,16 +1,6 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
MembershipOrg
} from '../models';
import {
validateClientForMembershipOrg,
validateMembershipOrg
} from '../helpers/membershipOrg';
// TODO: transform
import { validateClientForMembershipOrg } from '../validation';
type req = 'params' | 'body' | 'query';
@@ -1,9 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { IOrganization, MembershipOrg } from '../models';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
import { validateMembershipOrg } from '../helpers/membershipOrg';
import { validateClientForOrganization } from '../helpers/organization';
import { validateClientForOrganization } from '../validation';
type req = 'params' | 'body' | 'query';
+1 -8
View File
@@ -1,13 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors';
import { Secret } from '../models';
import {
validateMembership
} from '../helpers/membership';
import {
validateClientForSecret
} from '../helpers/secrets';
import { validateClientForSecret } from '../validation';
// note: used for old /v1/secret and /v2/secret routes.
// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception
+1 -3
View File
@@ -1,8 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models';
import { validateClientForSecrets } from '../helpers/secrets';
import { validateClientForSecrets } from '../validation';
const requireSecretsAuth = ({
acceptedRoles,
@@ -1,15 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceAccount } from '../models';
import {
ServiceAccountNotFoundError
} from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
validateClientForServiceAccount
} from '../helpers/serviceAccount';
import { validateClientForServiceAccount } from '../validation';
type req = 'params' | 'body' | 'query';
@@ -1,9 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceToken, ServiceTokenData } from '../models';
import { validateClientForServiceTokenData } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
import { validateClientForServiceTokenData } from '../validation';
type req = 'params' | 'body' | 'query';
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { validateClientForWorkspace } from '../helpers/workspace';
import { validateClientForWorkspace } from '../validation';
type req = 'params' | 'body' | 'query';
+20
View File
@@ -1,4 +1,9 @@
import { Schema, model, Types } from 'mongoose';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
export interface IBackupPrivateKey {
_id: Types.ObjectId;
@@ -7,6 +12,8 @@ export interface IBackupPrivateKey {
iv: string;
tag: string;
salt: string;
algorithm: string;
keyEncoding: 'base64' | 'utf8';
verifier: string;
}
@@ -32,6 +39,19 @@ const backupPrivateKeySchema = new Schema<IBackupPrivateKey>(
select: false,
required: true
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
},
salt: {
type: String,
select: false,
+23
View File
@@ -1,4 +1,10 @@
import { Schema, model, Types } from 'mongoose';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_HEX,
ENCODING_SCHEME_BASE64
} from '../variables';
export interface IBot {
_id: Types.ObjectId;
@@ -9,6 +15,8 @@ export interface IBot {
encryptedPrivateKey: string;
iv: string;
tag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'base64' | 'utf8';
}
const botSchema = new Schema<IBot>(
@@ -45,6 +53,21 @@ const botSchema = new Schema<IBot>(
type: String,
required: true,
select: false
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true,
select: false
}
},
{
+18
View File
@@ -14,6 +14,9 @@ import {
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from "../variables";
export interface IIntegrationAuth extends Document {
@@ -31,6 +34,8 @@ export interface IIntegrationAuth extends Document {
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
algorithm?: 'aes-256-gcm';
keyEncoding?: 'utf8' | 'base64';
accessExpiresAt?: Date;
}
@@ -109,6 +114,19 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
type: Date,
select: false,
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
}
},
{
timestamps: true,
+18
View File
@@ -2,6 +2,9 @@ import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
import { ROOT_FOLDER_PATH } from '../utils/folder';
@@ -25,6 +28,8 @@ export interface ISecret {
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'utf8' | 'base64';
tags?: string[];
path?: string;
folder?: Types.ObjectId;
@@ -111,6 +116,19 @@ const secretSchema = new Schema<ISecret>(
type: String,
required: false
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
},
// the full path to the secret in relation to folders
path: {
type: String,
+24 -1
View File
@@ -1,4 +1,9 @@
import { Schema, model, Types, Document } from 'mongoose';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
export interface ISecretBlindIndexData extends Document {
_id: Types.ObjectId;
@@ -6,6 +11,8 @@ export interface ISecretBlindIndexData extends Document {
encryptedSaltCiphertext: string;
saltIV: string;
saltTag: string;
algorithm: 'aes-256-gcm';
keyEncoding: 'base64' | 'utf8'
}
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
@@ -15,7 +22,7 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
ref: 'Workspace',
required: true
},
encryptedSaltCiphertext: {
encryptedSaltCiphertext: { // TODO: make these select: false
type: String,
required: true
},
@@ -26,7 +33,23 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
saltTag: {
type: String,
required: true
},
algorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true,
select: false
}
}
);
+1 -1
View File
@@ -7,9 +7,9 @@ import {
requireSecretsAuth,
validateRequest
} from '../../middleware';
import { validateClientForSecrets } from '../../validation';
import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2';
import { validateClientForSecrets } from '../../helpers/secrets';
import {
ADMIN,
MEMBER,
-12
View File
@@ -1,4 +1,3 @@
// WIP
import { Types } from 'mongoose';
import {
ISecret
@@ -11,7 +10,6 @@ import {
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
@@ -24,16 +22,6 @@ import {
} from '../helpers/secrets';
class SecretService {
/**
*
* @param param0 h
* @returns
*/
static async initSecretBlindIndexDataHelper() {
return await initSecretBlindIndexDataHelper();
}
/**
* Create secret blind index data containing encrypted blind index salt
+7 -1
View File
@@ -5,6 +5,7 @@
************************************************************************************************/
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
import { SecretService } from "../services";
import { Types } from 'mongoose';
import { getNodeEnv } from '../config';
@@ -119,7 +120,12 @@ export const createTestUserForDevelopment = async () => {
// create workspace if not exist
const workspaceInDB = await Workspace.findById(testWorkspaceId)
if (!workspaceInDB) {
await Workspace.create(testWorkspace)
const workspace = await Workspace.create(testWorkspace)
// initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id
});
}
// create workspace key if not exist
-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
}
}
);
}
+78
View File
@@ -0,0 +1,78 @@
import * as Sentry from '@sentry/node';
import { DatabaseService, TelemetryService } from '../../services';
import { setTransporter } from '../../helpers/nodemailer';
import { EELicenseService } from '../../ee/services';
import { initSmtp } from '../../services/smtp';
import { createTestUserForDevelopment } from '../addDevelopmentUser'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('../patchAsyncRoutes');
import {
validateEncryptionKeysConfig
} from './validateConfig';
import {
backfillSecretVersions,
backfillBots,
backfillSecretBlindIndexData,
backfillEncryptionMetadata
} from './backfillData';
import {
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts
} from './reencryptData';
import {
getNodeEnv,
getMongoURL,
getSentryDSN
} from '../../config';
/**
* Prepare Infisical upon startup. This includes tasks like:
* - Log initial telemetry message
* - Initializing SMTP configuration
* - Initializing the instance global feature set (if applicable)
* - Initializing the database connection
* - Initializing Sentry
* - Backfilling data
* - Re-encrypting data
*/
export const setup = async () => {
patchRouterParam();
await validateEncryptionKeysConfig();
await TelemetryService.logTelemetryMessage();
// initializing SMTP configuration
setTransporter(await initSmtp());
// initializing global feature set
await EELicenseService.initGlobalFeatureSet();
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
/**
* NOTE: the order in this setup function is critical.
* It is important to backfill data before performing any re-encryption functionality.
*/
// backfilling data to catch up with new collections and updated fields
await backfillSecretVersions();
await backfillBots();
await backfillSecretBlindIndexData();
await backfillEncryptionMetadata();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
await reencryptBotPrivateKeys();
await reencryptSecretBlindIndexDataSalts();
// initializing Sentry
Sentry.init({
dsn: await getSentryDSN(),
tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === 'production' ? false : true,
environment: (await getNodeEnv())
});
await createTestUserForDevelopment();
}
+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
} from '../utils/errors';
import { IntegrationService } from '../services';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateUserClientForWorkspace } from './user';
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
/**
* Validate authenticated clients for integration authorization with id [integrationAuthId] based
+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,
ISecret,
IOrganization,
IServiceAccountWorkspacePermission,
ServiceAccountWorkspacePermission
} from '../models';
import { validateUserClientForServiceAccount } from './user';
import {
BadRequestError,
UnauthorizedRequestError,
@@ -25,11 +25,8 @@ import {
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForServiceAccount
} from '../helpers/user';
const validateClientForServiceAccount = async ({
export const validateClientForServiceAccount = async ({
authData,
serviceAccountId,
requiredPermissions
@@ -100,7 +97,7 @@ const validateClientForServiceAccount = async ({
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForWorkspace = async ({
export const validateServiceAccountClientForWorkspace = async ({
serviceAccount,
workspaceId,
environment,
@@ -169,7 +166,7 @@ const validateClientForServiceAccount = async ({
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForSecrets = async ({
export const validateServiceAccountClientForSecrets = async ({
serviceAccount,
secrets,
requiredPermissions
@@ -226,7 +223,7 @@ const validateClientForServiceAccount = async ({
* @param {ServiceAccount} targetServiceAccount - target service account to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForServiceAccount = ({
export const validateServiceAccountClientForServiceAccount = ({
serviceAccount,
targetServiceAccount,
requiredPermissions
@@ -248,7 +245,7 @@ const validateServiceAccountClientForServiceAccount = ({
* @param {User} obj.user - service account client
* @param {Organization} obj.organization - organization to validate against
*/
const validateServiceAccountClientForOrganization = async ({
export const validateServiceAccountClientForOrganization = async ({
serviceAccount,
organization
}: {
@@ -260,12 +257,4 @@ const validateServiceAccountClientForOrganization = async ({
message: 'Failed service account authorization for the given organization'
});
}
}
export {
validateClientForServiceAccount,
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets,
validateServiceAccountClientForServiceAccount,
validateServiceAccountClientForOrganization
}
@@ -18,8 +18,8 @@ import {
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateUserClientForWorkspace } from './user';
import { validateServiceAccountClientForWorkspace } from './serviceAccount';
/**
* Validate authenticated clients for service token with id [serviceTokenId] based
@@ -29,7 +29,7 @@ import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAcco
* @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForServiceTokenData = async ({
export const validateClientForServiceTokenData = async ({
authData,
serviceTokenDataId,
acceptedRoles
@@ -100,7 +100,7 @@ const validateClientForServiceTokenData = async ({
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForWorkspace = async ({
export const validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
@@ -146,7 +146,7 @@ const validateClientForServiceTokenData = async ({
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForSecrets = async ({
export const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData,
secrets,
requiredPermissions
@@ -179,10 +179,4 @@ const validateClientForServiceTokenData = async ({
}
});
});
}
export {
validateClientForServiceTokenData,
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}
+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';
const ACTION_LOGOUT = 'logout';
const ACTION_ADD_SECRETS = 'addSecrets';
const ACTION_DELETE_SECRETS = 'deleteSecrets';
const ACTION_UPDATE_SECRETS = 'updateSecrets';
const ACTION_READ_SECRETS = 'readSecrets';
export {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS
}
export const ACTION_LOGIN = 'login';
export const ACTION_LOGOUT = 'logout';
export const ACTION_ADD_SECRETS = 'addSecrets';
export const ACTION_DELETE_SECRETS = 'deleteSecrets';
export const ACTION_UPDATE_SECRETS = 'updateSecrets';
export const ACTION_READ_SECRETS = 'readSecrets';
+4 -11
View File
@@ -1,11 +1,4 @@
const AUTH_MODE_JWT = 'jwt';
const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
export {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
}
export const AUTH_MODE_JWT = 'jwt';
export const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
export const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
export const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
+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
const ENV_DEV = 'dev';
const ENV_TESTING = 'test';
const ENV_STAGING = 'staging';
const ENV_PROD = 'prod';
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
export {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET
}
export const ENV_DEV = 'dev';
export const ENV_TESTING = 'test';
export const ENV_STAGING = 'staging';
export const ENV_PROD = 'prod';
export const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
+2 -7
View File
@@ -1,7 +1,2 @@
const EVENT_PUSH_SECRETS = 'pushSecrets';
const EVENT_PULL_SECRETS = 'pullSecrets';
export {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
}
export const EVENT_PUSH_SECRETS = 'pushSecrets';
export const EVENT_PULL_SECRETS = 'pullSecrets';
+13 -154
View File
@@ -1,154 +1,13 @@
import {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET,
} from "./environment";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
getIntegrationOptions
} from "./integration";
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from './action';
import {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL
} from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
import {
MFA_METHOD_EMAIL
} from './user';
import {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
} from './token';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from './permission';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from './authentication';
export {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
getIntegrationOptions,
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL,
PLAN_STARTER,
PLAN_PRO,
MFA_METHOD_EMAIL,
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
};
export * from './action';
export * from './authentication';
export * from './crypto';
export * from './environment';
export * from './event';
export * from './integration';
export * from './organization';
export * from './permission';
export * from './secret';
export * from './smtp';
export * from './stripe';
export * from './token';
export * from './user';
+34 -71
View File
@@ -8,21 +8,21 @@ import {
} from '../config';
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = "github";
const INTEGRATION_GITLAB = "gitlab";
const INTEGRATION_RENDER = "render";
const INTEGRATION_RAILWAY = "railway";
const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_TRAVISCI = "travisci";
const INTEGRATION_SUPABASE = 'supabase';
const INTEGRATION_SET = new Set([
export const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
export const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
export const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
export const INTEGRATION_HEROKU = "heroku";
export const INTEGRATION_VERCEL = "vercel";
export const INTEGRATION_NETLIFY = "netlify";
export const INTEGRATION_GITHUB = "github";
export const INTEGRATION_GITLAB = "gitlab";
export const INTEGRATION_RENDER = "render";
export const INTEGRATION_RAILWAY = "railway";
export const INTEGRATION_FLYIO = "flyio";
export const INTEGRATION_CIRCLECI = "circleci";
export const INTEGRATION_TRAVISCI = "travisci";
export const INTEGRATION_SUPABASE = 'supabase';
export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
@@ -37,31 +37,31 @@ const INTEGRATION_SET = new Set([
]);
// integration types
const INTEGRATION_OAUTH2 = "oauth2";
export const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
export const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`;
export const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
export const INTEGRATION_VERCEL_TOKEN_URL =
"https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL =
export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
export const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_token";
const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
// integration apps endpoints
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
export const INTEGRATION_RENDER_API_URL = "https://api.render.com";
export const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
const getIntegrationOptions = async () => {
export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [
{
name: 'Heroku',
@@ -202,41 +202,4 @@ const getIntegrationOptions = async () => {
]
return INTEGRATION_OPTIONS;
}
export {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
getIntegrationOptions
};
}
+5 -7
View File
@@ -1,12 +1,10 @@
// membership roles
const OWNER = "owner";
const ADMIN = "admin";
const MEMBER = "member";
export const OWNER = "owner";
export const ADMIN = "admin";
export const MEMBER = "member";
// membership statuses
const INVITED = "invited";
export const INVITED = "invited";
// -- organization
const ACCEPTED = "accepted";
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED };
export const ACCEPTED = "accepted";
+2 -7
View File
@@ -1,7 +1,2 @@
const PERMISSION_READ_SECRETS = 'read';
const PERMISSION_WRITE_SECRETS = 'write';
export {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
}
export const PERMISSION_READ_SECRETS = 'read';
export const PERMISSION_WRITE_SECRETS = 'write';
+2 -7
View File
@@ -1,8 +1,3 @@
// secrets
const SECRET_SHARED = 'shared';
const SECRET_PERSONAL = 'personal';
export {
SECRET_SHARED,
SECRET_PERSONAL
}
export const SECRET_SHARED = 'shared';
export const SECRET_PERSONAL = 'personal';
+5 -13
View File
@@ -1,13 +1,5 @@
const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
const SMTP_HOST_GMAIL = 'smtp.gmail.com';
export {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL
}
export const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
export const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
export const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
export const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
export const SMTP_HOST_GMAIL = 'smtp.gmail.com';
+2 -7
View File
@@ -1,7 +1,2 @@
const PLAN_STARTER = 'starter';
const PLAN_PRO = 'pro';
export {
PLAN_STARTER,
PLAN_PRO
}
export const PLAN_STARTER = 'starter';
export const PLAN_PRO = 'pro';
+4 -11
View File
@@ -1,11 +1,4 @@
const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
const TOKEN_EMAIL_MFA = 'emailMfa';
const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
export {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
}
export const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
export const TOKEN_EMAIL_MFA = 'emailMfa';
export const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
export const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
+1 -5
View File
@@ -1,5 +1 @@
const MFA_METHOD_EMAIL = 'email';
export {
MFA_METHOD_EMAIL
}
export const MFA_METHOD_EMAIL = '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_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364"
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
const axios = require('axios');
import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser";
import { encryptSymmetric } from "../../src/utils/crypto";
import {
encryptSymmetric128BitHexKeyUTF8
} from '../../src/utils/crypto';
interface TokenData {
token: string;
@@ -64,7 +66,7 @@ export const getJWTFromTestUser = (): Promise<TokenData> => {
export const getServiceTokenFromTestUser = async () => {
const loggedInUserDetails = await getJWTFromTestUser()
const randomBytes = crypto.randomBytes(16).toString('hex');
const { ciphertext, iv, tag } = encryptSymmetric({
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: plainTextWorkspaceKey,
key: randomBytes,
});
@@ -1,9 +1,7 @@
import { describe, test, expect } from '@jest/globals';
import {
decryptAsymmetric,
decryptSymmetric,
encryptAsymmetric,
encryptSymmetric
} from '../../../src/utils/crypto';
describe('Crypto', () => {
@@ -153,99 +151,4 @@ describe('Crypto', () => {
});
});
});
describe('encryptSymmetric', () => {
let plaintext: string;
const key = '7e8ee7e5cc667b9c1829783ad31f36f4';
test('should encrypt plaintext with the given key', () => {
plaintext = 'secret-message';
const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key });
expect(ciphertext).toBeDefined();
expect(iv).toBeDefined();
expect(tag).toBeDefined();
});
test('should throw an error when plaintext is undefined', () => {
const invalidKey = 'invalid-key';
expect(() => {
encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Invalid key length');
});
test('should throw an error when invalid key is provided', () => {
plaintext = 'secret-message';
const invalidKey = 'invalid-key';
expect(() => {
encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Invalid key length');
});
});
describe('decryptSymmetric', () => {
const plaintext = 'secret-message';
const key = '7e8ee7e5cc667b9c1829783ad31f36f4';
const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key });
test('should decrypt encrypted plaintext', () => {
const result = decryptSymmetric({
ciphertext,
iv,
tag,
key
});
expect(result).toBeDefined();
expect(result).toEqual(plaintext);
});
test('should fail if ciphertext is modified', () => {
const modifieldCiphertext = 'abcdefghijklmnopqrstuvwxyz';
expect(() => {
decryptSymmetric({
ciphertext: modifieldCiphertext,
iv,
tag,
key
});
}).toThrowError('Unsupported state or unable to authenticate data');
});
test('should fail if iv is modified', () => {
const modifiedIv = 'abcdefghijklmnopqrstuvwxyz';
expect(() => {
decryptSymmetric({
ciphertext,
iv: modifiedIv,
tag,
key
});
}).toThrowError('Unsupported state or unable to authenticate data');
});
test('should fail if tag is modified', () => {
const modifiedTag = 'abcdefghijklmnopqrstuvwxyz';
expect(() => {
decryptSymmetric({
ciphertext,
iv,
tag: modifiedTag,
key
});
}).toThrowError(/Invalid authentication tag length: \d+/);
});
test('should throw an error when decryption fails', () => {
const invalidKey = 'invalid-key';
expect(() => {
decryptSymmetric({
ciphertext,
iv,
tag,
key: invalidKey
});
}).toThrowError('Invalid key length');
});
});
});