Begin refactoring middleware for service accounts

This commit is contained in:
Tuan Dang
2023-04-04 11:08:03 +03:00
parent c797901778
commit aa53de9070
39 changed files with 943 additions and 577 deletions
+1 -82
View File
@@ -12,8 +12,8 @@
"@aws-sdk/client-secrets-manager": "^3.281.0", "@aws-sdk/client-secrets-manager": "^3.281.0",
"@godaddy/terminus": "^4.11.2", "@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/tracing": "^7.39.0",
"@sentry/node": "^7.40.0", "@sentry/node": "^7.40.0",
"@sentry/tracing": "^7.39.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0", "await-to-js": "^3.0.0",
@@ -2988,24 +2988,6 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
}, },
"node_modules/@sentry/core": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.38.0.tgz",
"integrity": "sha512-+hXh/SO3Ie6WC2b+wi01xLhyVREdkRXS5QBmCiv3z2ks2HvYXp7PoKSXJvNKiwCP+pBD+enOnM1YEzM2yEy5yw==",
"dependencies": {
"@sentry/types": "7.38.0",
"@sentry/utils": "7.38.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/core/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/node": { "node_modules/@sentry/node": {
"version": "7.40.0", "version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz",
@@ -3113,31 +3095,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}, },
"node_modules/@sentry/types": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.38.0.tgz",
"integrity": "sha512-NKOALR6pNUMzUrsk2m+dkPrO8uGNvNh1LD0BCPswKNjC2qHo1h1mDGCgBmF9+EWyii8ZoACTIsxvsda+MBf97Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.38.0.tgz",
"integrity": "sha512-MgbI3YmYuyyhUtvcXkgGBqjOW+nuLLNGUdWCK+C4kObf8VbLt3dSE/7SEMT6TSHLYQmxs2BxFgx5Agn97m68kQ==",
"dependencies": {
"@sentry/types": "7.38.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.25.24", "version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -14804,23 +14761,6 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
}, },
"@sentry/core": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.38.0.tgz",
"integrity": "sha512-+hXh/SO3Ie6WC2b+wi01xLhyVREdkRXS5QBmCiv3z2ks2HvYXp7PoKSXJvNKiwCP+pBD+enOnM1YEzM2yEy5yw==",
"requires": {
"@sentry/types": "7.38.0",
"@sentry/utils": "7.38.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/node": { "@sentry/node": {
"version": "7.40.0", "version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz",
@@ -14908,27 +14848,6 @@
} }
} }
}, },
"@sentry/types": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.38.0.tgz",
"integrity": "sha512-NKOALR6pNUMzUrsk2m+dkPrO8uGNvNh1LD0BCPswKNjC2qHo1h1mDGCgBmF9+EWyii8ZoACTIsxvsda+MBf97Q=="
},
"@sentry/utils": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.38.0.tgz",
"integrity": "sha512-MgbI3YmYuyyhUtvcXkgGBqjOW+nuLLNGUdWCK+C4kObf8VbLt3dSE/7SEMT6TSHLYQmxs2BxFgx5Agn97m68kQ==",
"requires": {
"@sentry/types": "7.38.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sinclair/typebox": { "@sinclair/typebox": {
"version": "0.25.24", "version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -9,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -38,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body; let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -112,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -181,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb'; import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
/** /**
* Create secret for workspace with id [workspaceId] and environment [environment] * Create secret for workspace with id [workspaceId] and environment [environment]
@@ -15,7 +15,7 @@ import { getPostHogClient } from '../../services';
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret; const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = { const sanitizedSecret: SanitizedSecretForCreate = {
@@ -68,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createSecrets = async (req: Request, res: Response) => { export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@@ -130,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds const secretIdsToDelete: string[] = req.body.secretIds
@@ -184,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecret = async (req: Request, res: Response) => { export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id) await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) { if (postHogClient) {
@@ -213,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecrets = async (req: Request, res: Response) => { export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@@ -281,7 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecret = async (req: Request, res: Response) => { export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@@ -335,7 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { environment } = req.query; const { environment } = req.query;
const { workspaceId } = req.params; const { workspaceId } = req.params;
+103 -96
View File
@@ -15,12 +15,12 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services'; import { EESecretService, EELogService } from '../../ee/services';
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog'; import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag'; import Tag from '../../models/tag';
import _ from 'lodash'; import _, { eq } from 'lodash';
import { import {
BatchSecretRequest, BatchSecretRequest,
BatchSecret BatchSecret
@@ -28,12 +28,13 @@ import {
/** /**
* Peform a batch of any specified CUD secret operations * Peform a batch of any specified CUD secret operations
* (used by dashboard)
* @param req * @param req
* @param res * @param res
*/ */
export const batchSecrets = async (req: Request, res: Response) => { export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']); const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const { const {
workspaceId, workspaceId,
@@ -91,7 +92,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id) secretIds: createdSecrets.map((n) => n._id)
}) as IAction; }) as IAction;
@@ -328,15 +331,16 @@ export const createSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body; const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE) if (req.user) {
const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, ABILITY_WRITE)
if (!hasAccess) { if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
} }
}
let listOfSecretsToCreate; let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) { if (Array.isArray(req.body.secrets)) {
@@ -378,7 +382,7 @@ export const createSecrets = async (req: Request, res: Response) => {
version: 1, version: 1,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
type, type,
user: type === SECRET_PERSONAL ? req.user : undefined, user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment, environment,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -391,7 +395,7 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentTag, secretCommentTag,
tags tags
}); });
}) });
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject()); const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
@@ -447,14 +451,18 @@ export const createSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id) secretIds: newlyCreatedSecrets.map((n) => n._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
addAction && await EELogService.createLog({ addAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction], actions: [addAction],
channel, channel,
@@ -466,10 +474,15 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId workspaceId
}); });
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets added', event: 'secrets added',
distinctId: req.user.email, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: listOfSecretsToCreate.length, numberOfSecrets: listOfSecretsToCreate.length,
environment, environment,
@@ -533,34 +546,36 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
*/ */
const postHogClient = getPostHogClient(); const { tagSlugs } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const { workspaceId, environment, tagSlugs } = req.query; // tags logic
let tagIds = [];
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : []; const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
let userId = "" // used for getting personal secrets for user if (tagNamesList != undefined && tagNamesList.length != 0) {
let userEmail = "" // used for posthog const workspaceFromDB = await Tag.find({ workspace: workspaceId });
tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
}
let secrets: ISecret[] = [];
if (req.user) { if (req.user) {
userId = req.user._id; // case: client authorization is via JWT
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped to env and project
let hasWriteOnlyAccess let hasWriteOnlyAccess
if (!req.serviceTokenData) { if (!req.serviceTokenData) {
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment) hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment) const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
if (hasNoAccess) { if (hasNoAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
} }
} }
let secrets: any
let secretQuery: any
let secretQuery: any;
if (tagNamesList != undefined && tagNamesList.length != 0) { if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId }) const workspaceFromDB = await Tag.find({ workspace: workspaceId })
@@ -573,7 +588,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspace: workspaceId, workspace: workspaceId,
environment, environment,
$or: [ $or: [
{ user: userId }, { user: req.user._id },
{ user: { $exists: false } } { user: { $exists: false } }
], ],
tags: { $in: tagIds }, tags: { $in: tagIds },
@@ -584,7 +599,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspace: workspaceId, workspace: workspaceId,
environment, environment,
$or: [ $or: [
{ user: userId }, { user: req.user._id },
{ user: { $exists: false } } { user: { $exists: false } }
], ],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
@@ -592,32 +607,57 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
if (hasWriteOnlyAccess) { if (hasWriteOnlyAccess) {
// (i.e. you don't get values to decrypt since you can only write)
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag") secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
} else { } else {
secrets = await Secret.find(secretQuery).populate("tags") secrets = await Secret.find(secretQuery).populate("tags")
} }
}
if (req.serviceAccount || req.serviceTokenData) {
// case: client authorization is either via service account or service token
secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
user: {
$exists: false
},
...(tagIds.length > 0 ? { tags: { $in: tagIds } } : {}),
type: SECRET_SHARED
});
}
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createAction({ const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
userId: new Types.ObjectId(userId), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id) secretIds: secrets.map((n: any) => n._id)
}); });
readAction && await EELogService.createLog({ readAction && await EELogService.createLog({
userId: new Types.ObjectId(userId), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction], actions: [readAction],
channel, channel,
ipAddress: req.ip ipAddress: req.ip
}); });
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pulled', event: 'secrets pulled',
distinctId: userEmail, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
@@ -633,59 +673,6 @@ export const getSecrets = async (req: Request, res: Response) => {
}); });
} }
export const getOnlySecretKeys = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.query;
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped
if (!req.serviceTokenData) {
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
const [err, secretKeys] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
)
.select("secretKeyIV secretKeyTag secretKeyCiphertext")
.then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
// readAction && await EELogService.createLog({
// userId: new Types.ObjectId(userId),
// workspaceId: new Types.ObjectId(workspaceId as string),
// actions: [readAction],
// channel,
// ipAddress: req.ip
// });
return res.status(200).send({
secretKeys
});
}
/** /**
* Update secret(s) * Update secret(s)
* @param req * @param req
@@ -736,10 +723,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
interface PatchSecret { interface PatchSecret {
id: string; id: string;
secretKeyCiphertext: string; secretKeyCiphertext: string;
@@ -865,14 +850,18 @@ export const updateSecrets = async (req: Request, res: Response) => {
const updateAction = await EELogService.createAction({ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
updateAction && await EELogService.createLog({ updateAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [updateAction], actions: [updateAction],
channel, channel,
@@ -884,10 +873,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: key workspaceId: key
}) })
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets modified', event: 'secrets modified',
distinctId: req.user.email, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
@@ -909,7 +903,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
/** /**
* Delete secret(s) with id [workspaceId] and environment [environment] * Delete secret(s)
* @param req * @param req
* @param res * @param res
*/ */
@@ -958,7 +952,11 @@ export const deleteSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
return res.status(200).send({
message: 'delete secrets!!'
});
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id); const toDelete = req.secrets.map((s: any) => s._id);
@@ -992,14 +990,18 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}); });
const deleteAction = await EELogService.createAction({ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId: req.user._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
deleteAction && await EELogService.createLog({ deleteAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [deleteAction], actions: [deleteAction],
channel, channel,
@@ -1011,10 +1013,15 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key workspaceId: key
}) })
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets deleted', event: 'secrets deleted',
distinctId: req.user.email, distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
@@ -195,10 +195,8 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
const { const {
environment, environment,
workspaceId, workspaceId,
canRead = false, read = false,
canWrite = false, write = false,
canUpdate = false,
canDelete = false,
encryptedKey, encryptedKey,
nonce nonce
} = req.body; } = req.body;
@@ -221,10 +219,8 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
serviceAccount: new Types.ObjectId(serviceAccountId), serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
canRead, read,
canWrite, write
canUpdate,
canDelete
}).save(); }).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({ const existingServiceAccountKey = await ServiceAccountKey.findOne({
@@ -19,7 +19,7 @@ import {
reformatPullSecrets reformatPullSecrets
} from '../../helpers/secret'; } from '../../helpers/secret';
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { getPostHogClient, EventService } from '../../services'; import { TelemetryService, EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
interface V2PushSecret { interface V2PushSecret {
@@ -48,7 +48,7 @@ interface V2PushSecret {
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body; let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -122,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
try { try {
const postHogClient = getPostHogClient(); const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
+33 -10
View File
@@ -24,11 +24,15 @@ import {
const createActionUpdateSecret = async ({ const createActionUpdateSecret = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
@@ -46,6 +50,8 @@ const createActionUpdateSecret = async ({
action = await new Action({ action = await new Action({
name, name,
user: userId, user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions
@@ -72,11 +78,15 @@ const createActionUpdateSecret = async ({
const createActionSecret = async ({ const createActionSecret = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
@@ -94,6 +104,8 @@ const createActionSecret = async ({
action = await new Action({ action = await new Action({
name, name,
user: userId, user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions
@@ -110,29 +122,36 @@ const createActionSecret = async ({
} }
/** /**
* Create an (audit) action for user with id [userId] * Create an (audit) action for client with id [userId],
* [serviceAccountId], or [serviceTokenDataId]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.name - name of action * @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action * @param {String} obj.userId - id of user associated with action
* @returns * @returns
*/ */
const createActionUser = ({ const createActionClient = ({
name, name,
userId userId,
serviceAccountId,
serviceTokenDataId
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
}) => { }) => {
let action; let action;
try { try {
action = new Action({ action = new Action({
name, name,
user: userId user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
}).save(); }).save();
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to create user action'); throw new Error('Failed to create client action');
} }
return action; return action;
@@ -149,11 +168,15 @@ const createActionUser = ({
const createActionHelper = async ({ const createActionHelper = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds, secretIds,
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) => { }) => {
@@ -162,7 +185,7 @@ const createActionHelper = async ({
switch (name) { switch (name) {
case ACTION_LOGIN: case ACTION_LOGIN:
case ACTION_LOGOUT: case ACTION_LOGOUT:
action = await createActionUser({ action = await createActionClient({
name, name,
userId userId
}); });
@@ -1,8 +1,9 @@
import { Types } from 'mongoose';
import _ from "lodash"; import _ from "lodash";
import { Membership } from "../../models"; import { Membership } from "../../models";
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
@@ -18,7 +19,7 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
return true return true
} }
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => { export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
@@ -36,7 +37,7 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
return false return false
} }
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => { export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return true return true
+7 -1
View File
@@ -16,12 +16,16 @@ import {
*/ */
const createLogHelper = async ({ const createLogHelper = async ({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
ipAddress ipAddress
}: { }: {
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
@@ -31,6 +35,8 @@ const createLogHelper = async ({
try { try {
log = await new Log({ log = await new Log({
user: userId, user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined, workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name), actionNames: actions.map((a) => a.name),
actions, actions,
+11 -2
View File
@@ -11,6 +11,8 @@ import {
export interface IAction { export interface IAction {
name: string; name: string;
user?: Types.ObjectId, user?: Types.ObjectId,
serviceAccount?: Types.ObjectId,
serviceTokenData?: Types.ObjectId,
workspace?: Types.ObjectId, workspace?: Types.ObjectId,
payload?: { payload?: {
secretVersions?: Types.ObjectId[] secretVersions?: Types.ObjectId[]
@@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User', ref: 'User'
required: true },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
+10
View File
@@ -11,6 +11,8 @@ import {
export interface ILog { export interface ILog {
_id: Types.ObjectId; _id: Types.ObjectId;
user?: Types.ObjectId; user?: Types.ObjectId;
serviceAccount?: Types.ObjectId;
serviceTokenData?: Types.ObjectId;
workspace?: Types.ObjectId; workspace?: Types.ObjectId;
actionNames: string[]; actionNames: string[];
actions: Types.ObjectId[]; actions: Types.ObjectId[];
@@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User' ref: 'User'
}, },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace' ref: 'Workspace'
+14 -2
View File
@@ -26,12 +26,16 @@ class EELogService {
*/ */
static async createLog({ static async createLog({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
ipAddress ipAddress
}: { }: {
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
@@ -40,6 +44,8 @@ class EELogService {
if (!EELicenseService.isLicenseValid) return null; if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({ return await createLogHelper({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
@@ -59,17 +65,23 @@ class EELogService {
static async createAction({ static async createAction({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) { }) {
return await createActionHelper({ return await createActionHelper({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}); });
+16 -11
View File
@@ -22,6 +22,12 @@ import {
getJwtRefreshLifetime, getJwtRefreshLifetime,
getJwtRefreshSecret getJwtRefreshSecret
} from '../config'; } from '../config';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/** /**
* *
@@ -39,7 +45,7 @@ const validateAuthMode = ({
const apiKey = headers['x-api-key']; const apiKey = headers['x-api-key'];
const authHeader = headers['authorization']; const authHeader = headers['authorization'];
let authTokenType, authTokenValue; let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) { if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present // case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' }); throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
@@ -47,7 +53,7 @@ const validateAuthMode = ({
if (typeof apiKey === 'string') { if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key) // case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey'; authMode = AUTH_MODE_API_KEY;
authTokenValue = apiKey; authTokenValue = apiKey;
} }
@@ -63,24 +69,24 @@ const validateAuthMode = ({
switch (tokenValue.split('.', 1)[0]) { switch (tokenValue.split('.', 1)[0]) {
case 'st': case 'st':
authTokenType = 'serviceToken'; authMode = AUTH_MODE_SERVICE_TOKEN;
break; break;
case 'sa': case 'sa':
authTokenType = 'serviceAccount'; authMode = AUTH_MODE_SERVICE_ACCOUNT;
break; break;
default: default:
authTokenType = 'jwt'; authMode = AUTH_MODE_JWT;
} }
authTokenValue = tokenValue; authTokenValue = tokenValue;
} }
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' }); if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' }); if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({ return ({
authTokenType, authMode,
authTokenValue authTokenValue
}); });
} }
@@ -155,8 +161,7 @@ const getAuthSTDPayload = async ({
serviceTokenData = await ServiceTokenData serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER) .findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag') .select('+encryptedKey +iv +tag');
.populate<{user: IUser}>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
@@ -216,7 +221,7 @@ const getAuthAPIKeyPayload = async ({
const apiKeyData = await APIKeyData const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey'); .populate<{user: IUser}>('user', '+publicKey');
if (!apiKeyData) { if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
-52
View File
@@ -24,57 +24,6 @@ import _ from 'lodash';
import { ABILITY_WRITE } from '../variables/organization'; import { ABILITY_WRITE } from '../variables/organization';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that user with id [userId] can modify secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.userId - id of user to validate
* @param {Object} obj.secretIds - secret ids
* @returns {Secret[]} secrets
*/
const validateSecrets = async ({
userId,
secretIds
}: {
userId: string;
secretIds: string[];
}) => {
let secrets;
try {
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Unable to validate some secrets' })
}
const userMemberships = await Membership.find({ user: userId })
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())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
}
} else {
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
}
});
} catch (err) {
throw BadRequestError({ message: 'Unable to validate secrets' })
}
return secrets;
}
interface V1PushSecret { interface V1PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
@@ -714,7 +663,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
}; };
export { export {
validateSecrets,
v1PushSecrets, v1PushSecrets,
v2PushSecrets, v2PushSecrets,
pullSecrets, pullSecrets,
+102
View File
@@ -0,0 +1,102 @@
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
Secret,
ISecret
} from '../models';
import {
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets
} from '../helpers/serviceAccount';
import { BadRequestError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate accepted clients for secrets with ids [secretIds]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {ServiceTokenData} obj.service - service token client
* @param {String[]} obj.secretIds - ids of secrets to validate against
*/
const validateClientForSecrets = async ({
authData,
secretIds,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
secretIds: string[];
requiredPermissions: string[];
}) => {
let secrets: ISecret[] = [];
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
// TODO
await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForSecrets({
serviceTokenData: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
}
return secrets;
}
export {
validateClientForSecrets
}
+63
View File
@@ -0,0 +1,63 @@
import _ from 'lodash';
import { Types } from 'mongoose';
import {
IServiceAccount,
ISecret,
ServiceAccountWorkspacePermission
} from '../models';
/**
* Validate that serviceAccount (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {Object} obj.
*/
const validateServiceAccountClientForWorkspace = async ({
serviceAccount,
workspaceId,
environment,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
workspaceId: Types.ObjectId;
environment: string;
requiredPermissions: string[];
}) => {
// TODO
return [];
}
/**
* Validate that service account (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceAccount - service account client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForSecrets = async ({
serviceAccount,
secrets,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
const permissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: serviceAccount._id
});
const permissionsObj = _.keyBy(permissions, (p) => {
return `${p.workspace.toString()}-${p.environment}`
});
// TODO
return [];
}
export {
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets
}
+99
View File
@@ -0,0 +1,99 @@
import { Types } from 'mongoose';
import {
ISecret,
IServiceTokenData
} from '../models';
import { UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token 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 validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
}
/**
* Validate that service token (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData,
secrets,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
secrets.forEach((secret: ISecret) => {
if (!serviceTokenData.workspace.equals(secret.workspace)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== secret.environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
});
}
export {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}
View File
+100 -2
View File
@@ -1,6 +1,18 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { IUser, User } from '../models'; import { Types } from 'mongoose';
import {
IUser,
ISecret,
User,
Membership
} from '../models';
import { sendMail } from './nodemailer'; import { sendMail } from './nodemailer';
import { validateMembership } from './membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import {
ABILITY_WRITE
} from '../variables/organization';
/** /**
* Initialize a user under email [email] * Initialize a user under email [email]
@@ -146,4 +158,90 @@ const checkUserDevice = async ({
} }
} }
export { setupAccount, completeAccount, checkUserDevice }; /**
* 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,
requiredPermissions
}: {
user: IUser;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
// org-level and workspace-level permissions? - workspace-level env scoped?
// validate user membership in workspace
const membership = await validateMembership({
userId: user._id,
workspaceId
});
// validate user permission
// TODO: validate that user can perform action on environment in workspace
return membership;
}
/**
* Validate that user (client) can access secrets with ids [secretIds]
* 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: consider refactor
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())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
} else {
throw BadRequestError({
message: 'You cannot edit secrets of a workspace you are not a member of'
});
}
});
}
export {
setupAccount,
completeAccount,
checkUserDevice,
validateUserClientForWorkspace,
validateUserClientForSecrets
};
+70 -19
View File
@@ -5,47 +5,98 @@ import {
Bot, Bot,
Membership, Membership,
Key, Key,
Secret Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
} from '../models'; } from '../models';
import { createBot } from '../helpers/bot'; import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/** /**
* Validate accepted clients by id including [userId], [serviceAccountId], * Validate accepted clients for workspace with id [workspaceId] based
* and [serviceTokenDataId] for workspace with id [workspaceId] based
* on any known permissions. * on any known permissions.
* @param {Object} obj * @param {Object} obj
* @param {Types.ObjectId} obj.userId - id of user * @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/ */
const validateClientForWorkspace = async ({ const validateClientForWorkspace = async ({
userId, authData,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
environment environment,
requiredPermissions
}: { }: {
userId?: Types.ObjectId; authData: {
serviceAccountId?: Types.ObjectId; authMode: string;
serviceTokenDataId?: Types.ObjectId; authPayload: IUser | IServiceAccount | IServiceTokenData;
},
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
requiredPermissions?: string[];
}) => { }) => {
let membership; let membership;
if (userId) { if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
membership = await validateMembership({ membership = await validateUserClientForWorkspace({
userId, user: authData.authPayload,
workspaceId workspaceId,
environment,
requiredPermissions
}); });
// TODO: validate user against [requiredPermissions]
} }
if (serviceAccountId) { if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
// TODO const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId),
environment
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace environment'
});
// TODO: validate [requiredPermissions] against [permission]
} }
if (serviceTokenDataId) { if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
// TODO await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
// TODO: validate [requiredPermissions] against [permission]
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
} }
return ({ return ({
+2 -2
View File
@@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node';
import { DatabaseService } from './services'; import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health'; import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp'; import { initSmtp } from './services/smtp';
import { logTelemetryMessage } from './services'; import { TelemetryService } from './services';
import { setTransporter } from './helpers/nodemailer'; import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser'; import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -80,7 +80,7 @@ const main = async () => {
}); });
} }
logTelemetryMessage(); TelemetryService.logTelemetryMessage();
setTransporter(initSmtp()); setTransporter(initSmtp());
await DatabaseService.initDatabase(getMongoURL()); await DatabaseService.initDatabase(getMongoURL());
+31 -28
View File
@@ -10,6 +10,17 @@ import {
import { import {
UnauthorizedRequestError UnauthorizedRequestError
} from '../utils/errors'; } from '../utils/errors';
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -28,59 +39,51 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
const requireAuth = ({ const requireAuth = ({
acceptedAuthModes = ['jwt'], acceptedAuthModes = [AUTH_MODE_JWT],
requiredServiceTokenPermissions = []
}: { }: {
acceptedAuthModes: string[]; acceptedAuthModes: string[];
requiredServiceTokenPermissions?: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes] // validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue] // and return token type [authTokenType] and value [authTokenValue]
const { authTokenType, authTokenValue } = validateAuthMode({ const { authMode, authTokenValue } = validateAuthMode({
headers: req.headers, headers: req.headers,
acceptedAuthModes acceptedAuthModes
}); });
req.authTokenType = authTokenType; let authPayload: IUser | IServiceAccount | IServiceTokenData;
switch (authMode) {
// attach auth payloads case AUTH_MODE_SERVICE_ACCOUNT:
let serviceTokenData: any; authPayload = await getAuthSAAKPayload({
switch (authTokenType) {
case 'serviceAccount':
req.serviceAccount = await getAuthSAAKPayload({
authTokenValue authTokenValue
}); });
req.serviceAccount = authPayload;
break; break;
case 'serviceToken': case AUTH_MODE_SERVICE_TOKEN:
serviceTokenData = await getAuthSTDPayload({ authPayload = await getAuthSTDPayload({
authTokenValue authTokenValue
}); });
req.serviceTokenData = authPayload;
// TODO: bring this into a separate collection
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});
req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;
break; break;
case 'apiKey': case AUTH_MODE_API_KEY:
// TODO: deprecate API key authPayload = await getAuthAPIKeyPayload({
req.user = await getAuthAPIKeyPayload({
authTokenValue authTokenValue
}); });
req.user = authPayload;
break; break;
default: default:
req.user = await getAuthUserPayload({ authPayload = await getAuthUserPayload({
authTokenValue authTokenValue
}); });
req.user = authPayload;
break; break;
} }
req.authData = {
authMode,
authPayload
}
return next(); return next();
} }
} }
+16 -30
View File
@@ -1,48 +1,34 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models'; import { Secret, Membership } from '../models';
import { validateSecrets } from '../helpers/secret'; import { validateClientForSecrets } from '../helpers/secrets';
// TODO: make this work for delete route
const requireSecretsAuth = ({ const requireSecretsAuth = ({
acceptedRoles acceptedRoles,
requiredPermissions = []
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
requiredPermissions?: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
let secrets; let secretIds = [];
try {
if (Array.isArray(req.body.secrets)) { if (Array.isArray(req.body.secrets)) {
// case: validate multiple secrets secretIds = req.body.secrets.map((s: any) => s.id);
secrets = await validateSecrets({ } else if (typeof req.body.secrets === 'object') {
userId: req.user._id.toString(), secretIds = [req.body.secrets.id];
secretIds: req.body.secrets.map((s: any) => s.id)
});
} else if (typeof req.body.secrets === 'object') { // change this to check for object
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secrets.id]
});
} else if (Array.isArray(req.body.secretIds)) { } else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({ secretIds = req.body.secretIds;
userId: req.user._id.toString(),
secretIds: req.body.secretIds
});
} else if (typeof req.body.secretIds === 'string') { } else if (typeof req.body.secretIds === 'string') {
// case: validate secretIds secretIds = [req.body.secretIds];
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secretIds]
});
} }
req.secrets = secrets; req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: [req.body.secretIds],
requiredPermissions
});
return next(); return next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
}
} }
} }
+6 -18
View File
@@ -16,44 +16,32 @@ type req = 'params' | 'body' | 'query';
const requireWorkspaceAuth = ({ const requireWorkspaceAuth = ({
acceptedRoles, acceptedRoles,
locationWorkspaceId, locationWorkspaceId,
locationEnvironment = undefined locationEnvironment = undefined,
requiredPermissions = []
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
locationWorkspaceId: req; locationWorkspaceId: req;
locationEnvironment?: req | undefined; locationEnvironment?: req | undefined;
requiredPermissions?: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try {
// TODO: throw errors if workspaceId or environemnt are not present
const workspaceId = req[locationWorkspaceId]?.workspaceId; const workspaceId = req[locationWorkspaceId]?.workspaceId;
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
// validate clients // validate clients
const { membership } = await validateClientForWorkspace({ const { membership } = await validateClientForWorkspace({
userId: req.user?._id, authData: req.authData,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
requiredPermissions
}); });
if (membership) { if (membership) {
req.membership = membership; req.membership = membership;
} }
if (
req.serviceTokenData
&& req.serviceTokenData.workspace.toString() !== workspaceId
&& req.serviceTokenData.environment !== req.body.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
return next(); return next();
} catch (err) {
return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
}; };
}; };
@@ -5,10 +5,8 @@ export interface IServiceAccountWorkspacePermission extends Document {
serviceAccount: Types.ObjectId; serviceAccount: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; environment: string;
canRead: boolean; read: boolean;
canWrite: boolean; write: boolean;
canUpdate: boolean;
canDelete: boolean;
} }
const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorkspacePermission>( const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorkspacePermission>(
@@ -27,19 +25,11 @@ const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorksp
type: String, type: String,
required: true required: true
}, },
canRead: { read: {
type: Boolean, type: Boolean,
default: false default: false
}, },
canWrite: { write: {
type: Boolean,
default: false
},
canUpdate: {
type: Boolean,
default: false
},
canDelete: {
type: Boolean, type: Boolean,
default: false default: false
} }
+3 -2
View File
@@ -1,6 +1,7 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceTokenData { export interface IServiceTokenData extends Document {
_id: Types.ObjectId;
name: string; name: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; environment: string;
+4
View File
@@ -16,6 +16,7 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -31,6 +32,7 @@ router.put(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -47,6 +49,7 @@ router.delete(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN], acceptedRoles: [ADMIN],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -61,6 +64,7 @@ router.get(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN], acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
+23 -21
View File
@@ -8,12 +8,14 @@ import {
} from '../../middleware'; } from '../../middleware';
import { query, body } from 'express-validator'; import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2'; import { secretsController } from '../../controllers/v2';
import { validateSecrets } from '../../helpers/secret'; import { validateClientForSecrets } from '../../helpers/secrets';
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED SECRET_SHARED,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../variables'; } from '../../variables';
import { import {
BatchSecretRequest BatchSecretRequest
@@ -22,8 +24,7 @@ import {
router.post( router.post(
'/batch', '/batch',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
requiredServiceTokenPermissions: ['read', 'write']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -40,12 +41,11 @@ router.post(
.filter((secretId) => secretId !== undefined) .filter((secretId) => secretId !== undefined)
if (secretIds.length > 0) { if (secretIds.length > 0) {
const relevantSecrets = await validateSecrets({ req.secrets = await validateClientForSecrets({
userId: req.user._id.toString(), authData: req.authData,
secretIds secretIds,
requiredPermissions: []
}); });
req.secrets = relevantSecrets;
} }
} }
return true; return true;
@@ -100,12 +100,13 @@ router.post(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
requiredServiceTokenPermissions: ['write']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body' locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.createSecrets secretsController.createSecrets
); );
@@ -117,12 +118,13 @@ router.get(
query('tagSlugs'), query('tagSlugs'),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
requiredServiceTokenPermissions: ['read']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query' locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -157,11 +159,11 @@ router.patch(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
requiredServiceTokenPermissions: ['write']
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.updateSecrets secretsController.updateSecrets
); );
@@ -186,11 +188,11 @@ router.delete(
.isEmpty(), .isEmpty(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
requiredServiceTokenPermissions: ['write']
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.deleteSecrets secretsController.deleteSecrets
); );
+2 -4
View File
@@ -125,10 +125,8 @@ router.post(
param('serviceAccountId').exists().isString().trim(), param('serviceAccountId').exists().isString().trim(),
body('workspaceId').exists().isString().notEmpty(), body('workspaceId').exists().isString().notEmpty(),
body('environment').exists().isString().notEmpty(), body('environment').exists().isString().notEmpty(),
body('canRead').isBoolean().optional(), body('read').isBoolean().optional(),
body('canWrite').isBoolean().optional(), body('write').isBoolean().optional(),
body('canUpdate').isBoolean().optional(),
body('canDelete').isBoolean().optional(),
body('encryptedKey').exists().isString().notEmpty(), body('encryptedKey').exists().isString().notEmpty(),
body('nonce').exists().isString().notEmpty(), body('nonce').exists().isString().notEmpty(),
validateRequest, validateRequest,
+3 -1
View File
@@ -5,7 +5,7 @@ import { tagController } from '../../controllers/v2';
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest, validateRequest
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
@@ -16,6 +16,7 @@ router.get(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN], acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
@@ -39,6 +40,7 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN], acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('name').exists().trim(), body('name').exists().trim(),
-44
View File
@@ -1,44 +0,0 @@
import { PostHog } from 'posthog-node';
import { getLogger } from '../utils/logger';
import {
getNodeEnv,
getTelemetryEnabled,
getPostHogProjectApiKey,
getPostHogHost
} from '../config';
/**
* Logs telemetry enable/disable notice.
*/
const logTelemetryMessage = () => {
if(!getTelemetryEnabled()){
getLogger("backend-main").info([
"",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}
}
/**
* Return an instance of the PostHog client initialized.
* @returns
*/
const getPostHogClient = () => {
let postHogClient: any;
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(getPostHogProjectApiKey(), {
host: getPostHogHost()
});
}
return postHogClient;
}
export {
logTelemetryMessage,
getPostHogClient
}
+85
View File
@@ -0,0 +1,85 @@
import { PostHog } from 'posthog-node';
import { getLogger } from '../utils/logger';
import {
getNodeEnv,
getTelemetryEnabled,
getPostHogProjectApiKey,
getPostHogHost
} from '../config';
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../models';
import {
BadRequestError
} from '../utils/errors';
class Telemetry {
/**
* Logs telemetry enable/disable notice.
*/
static logTelemetryMessage = () => {
if(!getTelemetryEnabled()){
getLogger("backend-main").info([
"",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}
}
/**
* Return an instance of the PostHog client initialized.
* @returns
*/
static getPostHogClient = () => {
let postHogClient: any;
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(getPostHogProjectApiKey(), {
host: getPostHogHost()
});
}
return postHogClient;
}
/**
* Return a distinct id for client to be used for logging telemetry
*/
static getDistinctId = ({
user,
serviceAccount,
serviceTokenData
}: {
user?: IUser;
serviceAccount?: IServiceAccount;
serviceTokenData?: IServiceTokenData;
}) => {
let distinctId = '';
if (user) {
distinctId = user.email;
}
if (serviceAccount) {
distinctId = `sa.${serviceAccount._id}`;
}
if (serviceTokenData) {
distinctId = `st.${serviceTokenData._id}`;
}
if (distinctId === '') {
throw BadRequestError({
message: 'Failed to obtain distinct id for logging telemetry'
});
}
return distinctId;
}
}
export default Telemetry;
+5 -3
View File
@@ -1,13 +1,15 @@
import DatabaseService from './DatabaseService'; import DatabaseService from './DatabaseService';
import { logTelemetryMessage, getPostHogClient } from './PostHogClient'; // import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
import TelemetryService from './TelemetryService';
import BotService from './BotService'; import BotService from './BotService';
import EventService from './EventService'; import EventService from './EventService';
import IntegrationService from './IntegrationService'; import IntegrationService from './IntegrationService';
import TokenService from './TokenService'; import TokenService from './TokenService';
export { export {
logTelemetryMessage, TelemetryService,
getPostHogClient, // logTelemetryMessage,
// getPostHogClient,
DatabaseService, DatabaseService,
BotService, BotService,
EventService, EventService,
+1 -1
View File
@@ -24,7 +24,7 @@ declare global {
serviceTokenData: any; serviceTokenData: any;
apiKeyData: any; apiKeyData: any;
query?: any; query?: any;
authTokenType: string; authData: any;
} }
} }
} }
+11
View File
@@ -0,0 +1,11 @@
const AUTH_MODE_JWT = 'jwt';
const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
export {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
}
+17 -1
View File
@@ -63,6 +63,16 @@ import {
TOKEN_EMAIL_ORG_INVITATION, TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET TOKEN_EMAIL_PASSWORD_RESET
} from './token'; } from './token';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from './permission';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from './authentication';
export { export {
OWNER, OWNER,
@@ -113,6 +123,8 @@ export {
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
getIntegrationOptions, getIntegrationOptions,
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,
@@ -124,5 +136,9 @@ export {
TOKEN_EMAIL_CONFIRMATION, TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA, TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION, TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
}; };
+7
View File
@@ -0,0 +1,7 @@
const PERMISSION_READ_SECRETS = 'read';
const PERMISSION_WRITE_SECRETS = 'write';
export {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
}
@@ -31,20 +31,16 @@ export type ServiceAccountWorkspacePermission = {
serviceAccount: string; serviceAccount: string;
workspace: Workspace; workspace: Workspace;
environment: string; environment: string;
canRead: boolean; read: boolean;
canWrite: boolean; write: boolean;
canUpdate: boolean;
canDelete: boolean;
} }
export type CreateServiceAccountWorkspacePermissionDTO = { export type CreateServiceAccountWorkspacePermissionDTO = {
serviceAccountId: string; serviceAccountId: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
canRead: boolean; read: boolean;
canWrite: boolean; write: boolean;
canUpdate: boolean;
canDelete: boolean;
encryptedKey: string; encryptedKey: string;
nonce: string; nonce: string;
} }
@@ -48,10 +48,8 @@ const createProjectLevelPermissionSchema = yup.object({
workspace: yup.string().required().label('Workspace'), workspace: yup.string().required().label('Workspace'),
environment: yup.string().required().label('Environment'), environment: yup.string().required().label('Environment'),
permissions: yup.object().shape({ permissions: yup.object().shape({
canRead: yup.boolean().required(), read: yup.boolean().required(),
canWrite: yup.boolean().required(), write: yup.boolean().required()
canUpdate: yup.boolean().required(),
canDelete: yup.boolean().required(),
}).defined().required() }).defined().required()
}); });
@@ -91,7 +89,7 @@ export const SAProjectLevelPermissionsTable = ({
privateKey, privateKey,
workspace, workspace,
environment, environment,
permissions: { canRead, canWrite, canUpdate, canDelete } permissions: { read, write }
}: CreateProjectLevelPermissionForm) => { }: CreateProjectLevelPermissionForm) => {
// TODO: clean up / modularize this function // TODO: clean up / modularize this function
@@ -126,10 +124,8 @@ export const SAProjectLevelPermissionsTable = ({
serviceAccountId, serviceAccountId,
workspaceId: workspace, workspaceId: workspace,
environment, environment,
canRead, read,
canWrite, write,
canUpdate,
canDelete,
encryptedKey: ciphertext, encryptedKey: ciphertext,
nonce nonce
}); });
@@ -187,10 +183,8 @@ export const SAProjectLevelPermissionsTable = ({
_id, _id,
workspace, workspace,
environment, environment,
canRead, read,
canWrite, write
canUpdate,
canDelete
}) => { }) => {
const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name; const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name;
return ( return (
@@ -200,28 +194,14 @@ export const SAProjectLevelPermissionsTable = ({
<Td> <Td>
<Checkbox <Checkbox
id="isReadPermissionEnabled" id="isReadPermissionEnabled"
isChecked={canRead} isChecked={read}
isDisabled isDisabled
>{/**/}</Checkbox> >{/**/}</Checkbox>
</Td> </Td>
<Td> <Td>
<Checkbox <Checkbox
id="isWritePermissionEnabled" id="isWritePermissionEnabled"
isChecked={canWrite} isChecked={write}
isDisabled
>{/**/}</Checkbox>
</Td>
<Td>
<Checkbox
id="isUpdatePermissionEnabled"
isChecked={canUpdate}
isDisabled
>{/**/}</Checkbox>
</Td>
<Td>
<Checkbox
id="isDeletePermissionEnabled"
isChecked={canDelete}
isDisabled isDisabled
>{/**/}</Checkbox> >{/**/}</Checkbox>
</Td> </Td>
@@ -352,28 +332,18 @@ export const SAProjectLevelPermissionsTable = ({
control={control} control={control}
name="permissions" name="permissions"
defaultValue={{ defaultValue={{
canRead: true, read: true,
canWrite: false, write: false
canUpdate: false,
canDelete: false
}} }}
render={({ field: { onChange, value }, fieldState: { error }}) => { render={({ field: { onChange, value }, fieldState: { error }}) => {
const options = [ const options = [
{ {
label: 'Read (default)', label: 'Read (default)',
value: 'canRead' value: 'read'
}, },
{ {
label: 'Write', label: 'Write',
value: 'canWrite' value: 'write'
},
{
label: 'Update',
value: 'canUpdate'
},
{
label: 'Delete',
value: 'canDelete'
} }
]; ];