Checkpoint finish preliminary support for ROOT_ENCRYPTION_KEY

This commit is contained in:
Tuan Dang
2023-05-06 22:07:59 +03:00
parent 3d3d7c9821
commit 87e047a152
54 changed files with 1877 additions and 1569 deletions

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",
@@ -5331,14 +5331,6 @@
"node": ">=12"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6904,13 +6896,12 @@
}
},
"node_modules/infisical-node": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz",
"integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.2.1.tgz",
"integrity": "sha512-zEB0w5+1O0mv9qc68bq4f9jDjrtwdbqjJebnwodgy8U1XZElDXeMDQgSMCtgYan7JRmVlH6s/LM8X7kUF+67ZA==",
"dependencies": {
"axios": "^1.3.3",
"dotenv": "^16.0.3",
"node-cache": "^5.1.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
}
@@ -8404,17 +8395,6 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-cache": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
"dependencies": {
"clone": "2.x"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
@@ -17266,11 +17246,6 @@
"wrap-ansi": "^7.0.0"
}
},
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -18461,13 +18436,12 @@
"dev": true
},
"infisical-node": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz",
"integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.2.1.tgz",
"integrity": "sha512-zEB0w5+1O0mv9qc68bq4f9jDjrtwdbqjJebnwodgy8U1XZElDXeMDQgSMCtgYan7JRmVlH6s/LM8X7kUF+67ZA==",
"requires": {
"axios": "^1.3.3",
"dotenv": "^16.0.3",
"node-cache": "^5.1.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
}
@@ -19615,14 +19589,6 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node-cache": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
"requires": {
"clone": "2.x"
}
},
"node-fetch": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",

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",

View File

@@ -1,24 +1,22 @@
import InfisicalClient from 'infisical-node';
import { validateEncryptionKey } from '../validation';
const client = new InfisicalClient({
export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!
});
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue == undefined ? false : (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue;
export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue; // TODO: deprecate in favor of INFISICAL_ENCRYPTION_KEY
export const getRootEncryptionKey = async (): Promise<string | undefined> => {
const encryptionKey = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue;
if (encryptionKey) {
// validate [encryptionKey] to make sure it is in base64 format and 256-bit
validateEncryptionKey(encryptionKey);
return encryptionKey;
}
return encryptionKey;
export const getEncryptionKey = async () => {
const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;

View File

@@ -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

View File

@@ -47,7 +47,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
error: 'Failed to send email verification code'
});
}
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});

View File

@@ -9,7 +9,9 @@ import {
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
ACTION_DELETE_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../../variables';
import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services';
@@ -81,7 +83,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId),
path: fullFolderPath,
folder: folderId,
secretBlindIndex
secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
break;
case 'PATCH':
@@ -96,6 +100,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
secretBlindIndex,
folder: folderId,
path: fullFolderPath,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
break;
case 'DELETE':
@@ -196,6 +202,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: u.secretCommentCiphertext,
secretCommentIV: u.secretCommentIV,
secretCommentTag: u.secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags
}));
@@ -444,6 +452,8 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags
});
})
@@ -490,7 +500,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}))
});
@@ -831,6 +843,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
...((
secretCommentCiphertext !== undefined &&
@@ -884,6 +898,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags: tags ? tags : secret.tags
});
})

View File

@@ -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

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

View File

@@ -41,7 +41,6 @@ const validateAuthMode = ({
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[]
}) => {
// TODO: refactor middleware
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];

View File

@@ -4,12 +4,7 @@ import {
BotKey,
Secret,
ISecret,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
IUser
} from "../models";
import {
generateKeyPair,
@@ -19,91 +14,16 @@ import {
} from '../utils/crypto';
import {
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from "../variables";
import { getEncryptionKey } from "../config";
import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors";
import { validateUserClientForWorkspace } from "../helpers/user";
import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount";
/**
* Validate authenticated clients for bot with id [botId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.botId - id of bot to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForBot = async ({
authData,
botId,
acceptedRoles,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
botId: Types.ObjectId;
acceptedRoles: Array<"admin" | "member">;
}) => {
const bot = await Bot.findById(botId);
if (!bot) throw BotNotFoundError();
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: bot.workspace,
});
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for bot",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
return bot;
}
throw BotNotFoundError({
message: "Failed client authorization for bot",
});
};
import {
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { InternalServerError } from "../utils/errors";
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -118,23 +38,52 @@ const createBot = async ({
name: string;
workspaceId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: await getEncryptionKey(),
if (rootEncryptionKey) {
const {
ciphertext,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: await getEncryptionKey(),
});
return await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: 'Failed to create new bot due to missing encryption key'
});
const bot = await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
}).save();
return bot;
};
/**
@@ -188,34 +137,54 @@ const getSecretsHelper = async ({
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId,
}).populate<{ sender: IUser }>("sender", "publicKey");
})
.populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({
workspace: workspaceId,
}).select("+encryptedPrivateKey +iv +tag");
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
if (!bot) throw new Error("Failed to find bot");
if (!bot.isActive) throw new Error("Bot is not active");
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: await getEncryptionKey(),
});
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
// case: encoding scheme is base64
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
const key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
}
return key;
throw InternalServerError({
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
});
};
/**
@@ -276,7 +245,6 @@ const decryptSymmetricHelper = async ({
};
export {
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,

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,

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,

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,

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,94 +10,6 @@ import {
getStripeProductTeam,
getStripeProductStarter,
} from "../config";
import {
UnauthorizedRequestError,
OrganizationNotFoundError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "../helpers/user";
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
/**
* Validate accepted clients for organization with id [organizationId]
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
*/
const validateClientForOrganization = async ({
authData,
organizationId,
acceptedRoles,
acceptedStatuses,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
organizationId: Types.ObjectId;
acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<"invited" | "accepted">;
}) => {
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization",
});
}
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
return { organization, membershipOrg };
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForOrganization({
serviceAccount: authData.authPayload,
organization,
});
return { organization };
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for organization",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
return { organization, membershipOrg };
}
throw UnauthorizedRequestError({
message: "Failed client authorization for organization",
});
};
/**
* Create an organization with name [name]
@@ -258,8 +157,7 @@ const updateSubscriptionOrgQuantity = async ({
};
export {
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity,
updateSubscriptionOrgQuantity
};

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
});
}),
});

View File

@@ -7,50 +7,27 @@ import {
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
AuthData
} from '../interfaces/middleware';
import {
User,
Workspace,
ServiceAccount,
ServiceTokenData,
Secret,
ISecret,
SecretBlindIndexData,
} from '../models';
import { SecretVersion } from '../ee/models';
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForSecret,
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets,
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets,
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError,
SecretBlindIndexDataNotFoundError
SecretBlindIndexDataNotFoundError,
InternalServerError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
SECRET_PERSONAL,
SECRET_SHARED,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
ACTION_DELETE_SECRETS,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../variables';
import crypto from 'crypto';
import * as argon2 from 'argon2';
@@ -58,7 +35,7 @@ import {
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8
} from '../utils/crypto';
import { getEncryptionKey } from '../config';
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
import { TelemetryService } from '../services';
import {
EESecretService,
@@ -69,157 +46,6 @@ import {
getAuthDataPayloadUserObj
} from '../utils/auth';
/**
* Validate authenticated clients for secrets with id [secretId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecret = async ({
authData,
secretId,
acceptedRoles,
requiredPermissions
}: {
authData: AuthData;
secretId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => {
const secret = await Secret.findById(secretId);
if (!secret) throw SecretNotFoundError({
message: 'Failed to find secret'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment
});
return secret;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secret'
});
}
/**
* Validate authenticated clients for secrets with ids [secretIds] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecrets = async ({
authData,
secretIds,
requiredPermissions
}: {
authData: AuthData;
secretIds: Types.ObjectId[];
requiredPermissions: string[];
}) => {
let secrets: ISecret[] = [];
secrets = await Secret.find({
_id: {
$in: secretIds
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForSecrets({
serviceTokenData: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secrets resource'
});
}
/**
* Create secret blind index data containing encrypted blind index [salt]
* for workspace with id [workspaceId]
@@ -231,26 +57,47 @@ const createSecretBlindIndexDataHelper = async ({
}: {
workspaceId: Types.ObjectId;
}) => {
// initialize random blind index salt for workspace
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt,
key: await getEncryptionKey()
});
const secretBlindIndexData = await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag
}).save();
return secretBlindIndexData;
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (rootEncryptionKey) {
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey);
return await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else {
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt,
key: encryptionKey
});
return await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
}
/**
@@ -264,22 +111,36 @@ const getSecretBlindIndexSaltHelper = async ({
}: {
workspaceId: Types.ObjectId;
}) => {
// check if workspace blind index data exists
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId
});
}).select('+algorithm +keyEncoding');
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt
const salt = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: await getEncryptionKey()
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
return client.decryptSymmetric(
secretBlindIndexData.encryptedSaltCiphertext,
rootEncryptionKey,
secretBlindIndexData.saltIV,
secretBlindIndexData.saltTag
);
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
// decrypt workspace salt
return decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: encryptionKey
});
}
throw InternalServerError({
message: 'Failed to obtain workspace salt needed for secret blind indexing'
});
return salt;
}
/**
@@ -422,7 +283,9 @@ const createSecretHelper = async ({
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
const secretVersion = new SecretVersion({
@@ -439,7 +302,9 @@ const createSecretHelper = async ({
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
// // (EE) add version for new secret
@@ -729,7 +594,9 @@ const updateSecretHelper = async ({
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
});
// (EE) add version for new secret
@@ -890,8 +757,6 @@ const deleteSecretHelper = async ({
}
export {
validateClientForSecret,
validateClientForSecrets,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,

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
};
}

View File

@@ -1,135 +1,14 @@
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { Types } from 'mongoose';
import {
Workspace,
Bot,
Membership,
Key,
Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
SecretBlindIndexData
Secret
} from '../models';
import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { SecretService } from '../services';
/**
* Validate authenticated clients for workspace with id [workspaceId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForWorkspace = async ({
authData,
workspaceId,
environment,
acceptedRoles,
requiredPermissions,
requireBlindIndicesEnabled
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: 'Failed to find workspace'
});
if (requireBlindIndicesEnabled) {
// case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: 'Failed workspace authorization due to blind indices not being enabled'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for workspace'
});
}
/**
* Create a workspace with name [name] in organization with id [organizationId]
* and a bot for it.
@@ -202,7 +81,6 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
};
export {
validateClientForWorkspace,
createWorkspace,
deleteWorkspace
};

View File

@@ -6,8 +6,6 @@ import helmet from 'helmet';
import cors from 'cors';
import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health';
import { TelemetryService } from './services';
import cookieParser from 'cookie-parser';
import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -72,10 +70,9 @@ import {
getSmtpHost
} from './config';
import { setup } from './utils/setup';
import { patchRouterParam } from './utils/patchAsyncRoutes';
const main = async () => {
TelemetryService.logTelemetryMessage();
await setup();
const app = express();
@@ -117,8 +114,8 @@ const main = async () => {
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
app.use('/api/v1/secret', v1SecretRouter); // deprecate
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
@@ -133,9 +130,9 @@ const main = async () => {
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/secret', v2SecretRouter); // deprecate
app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
@@ -146,7 +143,7 @@ const main = async () => {
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status
// server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code

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

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';

View File

@@ -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

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

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

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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { validateMembership } from '../helpers/membership';
import { validateClientForWorkspace } from '../helpers/workspace';
import { validateClientForWorkspace } from '../validation';
import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query';

View File

@@ -57,7 +57,8 @@ const botSchema = new Schema<IBot>(
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
required: true,
select: false
},
keyEncoding: {
type: String,
@@ -65,7 +66,8 @@ const botSchema = new Schema<IBot>(
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
required: true,
select: false
}
},
{

View File

@@ -22,7 +22,7 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
ref: 'Workspace',
required: true
},
encryptedSaltCiphertext: {
encryptedSaltCiphertext: { // TODO: make these select: false
type: String,
required: true
},
@@ -37,7 +37,8 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
algorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
required: true,
select: false
},
keyEncoding: {
type: String,
@@ -45,7 +46,8 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
],
required: true
required: true,
select: false
}
}

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,

View File

@@ -7,19 +7,14 @@ import {
IEncryptAsymmetricOutput,
IDecryptAsymmetricInput,
IEncryptSymmetricInput,
IEncryptSymmetricOutput,
IDecryptSymmetricInput
} from '../../interfaces/utils';
import {
BadRequestError,
InternalServerError
} from '../errors';
import { BadRequestError } from '../errors';
import {
ALGORITHM_AES_256_GCM,
NONCE_BYTES_SIZE,
BLOCK_SIZE_BYTES_16
} from '../../variables';
import { validateEncryptionKey } from '../../validation';
/**
* Return new base64, NaCl, public-private key pair.
@@ -96,70 +91,6 @@ const decryptAsymmetric = ({
return util.encodeUTF8(plaintext);
};
/**
* Return symmetrically encrypted [plaintext] using [key].
* @param {Object} obj
* @param {String} obj.plaintext - (utf8) plaintext to encrypt
* @param {String} obj.key - (base64) 256-bit key
* @returns {Object} obj
* @returns {String} obj.ciphertext (base64) ciphertext
* @returns {String} obj.iv (base64) iv
* @returns {String} obj.tag (base64) tag
*/
const encryptSymmetric = ({
plaintext,
key
}: IEncryptSymmetricInput): IEncryptSymmetricOutput => {
validateEncryptionKey(key);
const iv = crypto.randomBytes(NONCE_BYTES_SIZE);
const secretKey = crypto.createSecretKey(key, 'base64');
const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, secretKey, iv);
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
};
/**
* Return symmetrically decrypted [ciphertext] using [iv], [tag],
* and [key].
* @param {Object} obj
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.iv - (base64) 256-bit iv
* @param {String} obj.tag - (base64) tag
* @param {String} obj.key - (base64) 256-bit key
* @returns {String} cleartext - the deciphered ciphertext
*/
const decryptSymmetric = ({
ciphertext,
iv,
tag,
key
}: IDecryptSymmetricInput): string => {
validateEncryptionKey(key);
const secretKey = crypto.createSecretKey(key, 'base64');
const decipher = crypto.createDecipheriv(
ALGORITHM_AES_256_GCM,
secretKey,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
};
/**
* Return symmetrically encrypted [plaintext] using [key].
*
@@ -230,8 +161,6 @@ export {
generateKeyPair,
encryptAsymmetric,
decryptAsymmetric,
encryptSymmetric,
decryptSymmetric,
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8
};

View File

@@ -1,215 +0,0 @@
import crypto from 'crypto';
import { encryptSymmetric128BitHexKeyUTF8 } from '../crypto';
import { EESecretService } from '../../ee/services';
import { SecretVersion } from '../../ee/models';
import {
Secret,
ISecret,
SecretBlindIndexData,
Workspace,
Bot,
BackupPrivateKey,
IntegrationAuth
} from '../../models';
import { getEncryptionKey, getRootEncryptionKey } from '../../config';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../../variables';
/**
*
*/
export const backfillSecretVersions = async () => {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: "secretversions",
localField: "_id",
foreignField: "secret",
as: "versions",
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await EESecretService.addSecretVersions({
secretVersions: unversionedSecrets.map(
(s, idx) =>
new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment,
})
),
});
}
}
export const backfillSecretBlindIndexData = async () => {
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
_id: {
$nin: workspaceIdsBlindIndexed
}
});
const secretBlindIndexDataToInsert = await Promise.all(
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt,
key: await getEncryptionKey()
});
const secretBlindIndexData = new SecretBlindIndexData({
workspace: workspaceToBlindIndex,
encryptedSaltCiphertext,
saltIV,
saltTag
})
return secretBlindIndexData;
})
);
if (secretBlindIndexDataToInsert.length > 0) {
await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
}
}
export const backfillEncryptionMetadata = async () => {
// backfill bot encryption metadata
await Bot.updateMany(
{
algorithm: {
$exists: false
},
keySize: {
$exists: false
},
keyEncoding: {
$exists: false
}
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
);
// backfill secret blind index encryption metadata
await SecretBlindIndexData.updateMany(
{
algorithm: {
$exists: false
},
keySize: {
$exists: false
},
keyEncoding: {
$exists: false
}
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
);
// backfill backup private key encryption metadata
await BackupPrivateKey.updateMany(
{
algorithm: {
$exists: false
},
keySize: {
$exists: false
},
keyEncoding: {
$exists: false
}
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
);
// backfill integration auth encryption metadata
await IntegrationAuth.updateMany(
{
algorithm: {
$exists: false
},
keySize: {
$exists: false
},
keyEncoding: {
$exists: false
}
},
{
$set: {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
);
// TODO: blind indices
// TODO: secret versions and snapshots etc.
// TODO: re-encrypt keys logic
// TODO: how do you handle different parts of the software
// encrypting under different schemes?
// const encryptionKey = await getEncryptionKey();
// const rootEncryptionKey = await getRootEncryptionKey();
// console.log('rootEncryptionKey: ', rootEncryptionKey);
// if (encryptionKey && rootEncryptionKey) {
// // case: both the old encryption key and new encryption key are present
// // -> perform migration if needed
// console.log('rootEncryptionKey is defined');
// const bots = await Bot.find({
// algorithm: ALGORITHM_AES_256_GCM,
// keySize: 256,
// keyEncoding: ENCODING_SCHEME_UTF8
// }, 'encryptedPrivateKey iv tag');
// if (bots.length > 0) {
// // TODO: unencrypt and re-encrypt
// // TODO: unencrypt and re-encrypt blind-indices
// // probably then need to move this function
// console.log('bots: ', bots);
// }
// }
}

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
}
}
);
}

View File

@@ -1,15 +1,23 @@
import * as Sentry from '@sentry/node';
import { DatabaseService } from '../../services';
import { DatabaseService, TelemetryService } from '../../services';
import { setTransporter } from '../../helpers/nodemailer';
import { initSmtp } from '../../services/smtp';
import { createTestUserForDevelopment } from '../addDevelopmentUser'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('../patchAsyncRoutes');
import {
validateEncryptionKeysConfig
} from './validateConfig';
import {
backfillSecretVersions,
backfillBots,
backfillSecretBlindIndexData,
backfillEncryptionMetadata
} from './backfill';
} from './backfillData';
import {
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts
} from './reencryptData';
import {
getNodeEnv,
getMongoURL,
@@ -18,34 +26,48 @@ import {
/**
* Prepare Infisical upon startup. This includes tasks like:
* - Log initial telemetry message
* - Initializing SMTP configuration
* - Initializing the database connection
* - Initializing Sentry
* - Backfilling data
* - Re-encrypting data
*/
export const setup = async () => {
patchRouterParam();
await validateEncryptionKeysConfig();
await TelemetryService.logTelemetryMessage();
// initializing SMTP configuration
setTransporter(await initSmtp());
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
/**
* NOTE: the order in this setup function is critical.
* It is important to backfill data before performing any re-encryption functionality.
*/
// backfilling data
// backfilling data to catch up with new collections and updated fields
await backfillSecretVersions();
await backfillBots();
await backfillSecretBlindIndexData();
await backfillEncryptionMetadata();
// initializing Sentry
if ((await getNodeEnv()) !== 'development') {
Sentry.init({
dsn: await getSentryDSN(),
tracesSampleRate: 1.0,
debug: await getNodeEnv() === 'production' ? false : true,
environment: await getNodeEnv()
});
}
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
await reencryptBotPrivateKeys();
await reencryptSecretBlindIndexDataSalts();
// initializing Sentry
Sentry.init({
dsn: await getSentryDSN(),
tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === 'production' ? false : true,
environment: (await getNodeEnv())
});
patchRouterParam();
await createTestUserForDevelopment();
}

View File

@@ -0,0 +1,126 @@
import {
Bot,
IBot,
ISecretBlindIndexData,
SecretBlindIndexData
} from '../../models';
import { decryptSymmetric128BitHexKeyUTF8 } from '../../utils/crypto';
import {
client,
getEncryptionKey,
getRootEncryptionKey
} from '../../config';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from '../../variables';
/**
* Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY
*/
export const reencryptBotPrivateKeys = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (encryptionKey && rootEncryptionKey) {
// 1: re-encrypt bot private keys under ROOT_ENCRYPTION_KEY
const bots = await Bot.find({
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).select('+encryptedPrivateKey iv tag algorithm keyEncoding');
if (bots.length === 0) return;
const operationsBot = await Promise.all(
bots.map(async (bot: IBot) => {
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
});
const {
ciphertext: encryptedPrivateKey,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return ({
updateOne: {
filter: {
_id: bot._id
},
update: {
encryptedPrivateKey,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}
}
})
})
);
await Bot.bulkWrite(operationsBot);
}
}
/**
* Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY
*/
export const reencryptSecretBlindIndexDataSalts = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
// 2. re-encrypt secret blind index salts under ROOT_ENCRYPTION_KEY
if (encryptionKey && rootEncryptionKey) {
const secretBlindIndexData = await SecretBlindIndexData.find({
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).select('+encryptedSaltCiphertext +saltIV +saltTag +algorithm +keyEncoding');
if (secretBlindIndexData.length == 0) return;
const operationsSecretBlindIndexData = await Promise.all(
secretBlindIndexData.map(async (secretBlindIndexDatum: ISecretBlindIndexData) => {
const salt = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexDatum.encryptedSaltCiphertext,
iv: secretBlindIndexDatum.saltIV,
tag: secretBlindIndexDatum.saltTag,
key: encryptionKey
});
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey);
return ({
updateOne: {
filter: {
_id: secretBlindIndexDatum._id
},
update: {
encryptedSaltCiphertext,
saltIV,
saltTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
}
}
})
})
);
await SecretBlindIndexData.bulkWrite(operationsSecretBlindIndexData);
}
}

View File

@@ -0,0 +1,69 @@
import {
getEncryptionKey,
getRootEncryptionKey
} from '../../config';
import {
InternalServerError
} from '../../utils/errors';
/**
* Validate ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY. Specifically:
* - ENCRYPTION_KEY is a hex, 128-bit string
* - ROOT_ENCRYPTION_KEY is a base64, 128-bit string
* - Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY are present
*
* - Encrypted data is consistent with the passed in encryption keys
*
* NOTE 1: ENCRYPTION_KEY is being transitioned to ROOT_ENCRYPTION_KEY
* NOTE 2: In the future, we will have a superior validation function
* built into the SDK.
*/
export const validateEncryptionKeysConfig = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
// TODO: handle case where either of keys can be empty strings
// and it would actually count as being defined for encryption
// within the application
// console.log('validateEncryptionKeysConfig');
// console.log('encryptionKey: ', encryptionKey);
// console.log('rootEncryptionKey: ', rootEncryptionKey);
if (
(encryptionKey === undefined || encryptionKey === "") &&
(rootEncryptionKey === undefined || rootEncryptionKey === "")
) throw InternalServerError({
message: "Failed to find required root encryption key environment variable. Please make sure that you're passing in a ROOT_ENCRYPTION_KEY environment variable."
});
if (encryptionKey && encryptionKey !== '') {
// validate [encryptionKey]
const keyBuffer = Buffer.from(encryptionKey, 'hex');
const decoded = keyBuffer.toString('hex');
if (decoded !== encryptionKey) throw InternalServerError({
message: 'Failed to validate that the encryption key is correctly encoded in hex.'
});
if (keyBuffer.length !== 16) throw InternalServerError({
message: 'Failed to validate that the encryption key is a 128-bit hex string.'
});
}
if (rootEncryptionKey && rootEncryptionKey !== '') {
// validate [rootEncryptionKey]
const keyBuffer = Buffer.from(rootEncryptionKey, 'base64')
const decoded = keyBuffer.toString('base64');
if (decoded !== rootEncryptionKey) throw InternalServerError({
message: 'Failed to validate that the root encryption key is correctly encoded in base64'
});
if (keyBuffer.length !== 32) throw InternalServerError({
message: 'Failed to validate that the encryption key is a 256-bit base64 string'
});
}
}

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",
});
};

View File

@@ -1,21 +0,0 @@
import { InternalServerError } from "../utils/errors";
/**
* Validate that the encryption key [encryptionKey] is in base64 format and 256-bit
* @param {String} encryptionKey - the encryption key to validate
*/
export const validateEncryptionKey = (encryptionKey: string): Buffer => {
const keyBuffer = Buffer.from(encryptionKey, 'base64')
const decoded = keyBuffer.toString('base64');
if (decoded !== encryptionKey) throw InternalServerError({
message: 'Failed to validate the format of the encryption key. Please check that it is in base64 format.'
});
if (keyBuffer.length !== 32) throw InternalServerError({
message: 'Failed to validate that the encryption key is 256-bit. Please check that it is 256-bit.'
});
return keyBuffer;
};

View File

@@ -1 +1,10 @@
export * from './config';
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';

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'
});
}

View File

@@ -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

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'
});
}

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'
});
}

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",
});
};

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'
});
}

View File

@@ -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
}

View File

@@ -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
}

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;
}

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 });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for workspace'
});
}

View File

@@ -32,7 +32,6 @@ export default function SignUp() {
const router = useRouter();
const {data: serverDetails } = useFetchServerStatus()
const { t } = useTranslation();
useEffect(() => {