diff --git a/backend/package-lock.json b/backend/package-lock.json index c31a33ac6..5ec5a07b3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,8 +12,8 @@ "@aws-sdk/client-secrets-manager": "^3.281.0", "@godaddy/terminus": "^4.11.2", "@octokit/rest": "^19.0.5", - "@sentry/tracing": "^7.39.0", "@sentry/node": "^7.40.0", + "@sentry/tracing": "^7.39.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "await-to-js": "^3.0.0", @@ -2988,24 +2988,6 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "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": { "version": "7.40.0", "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", "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": { "version": "0.25.24", "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", "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": { "version": "7.40.0", "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": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index 1d1f8981c..4b377ec72 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -9,7 +9,7 @@ import { import { pushKeys } from '../../helpers/key'; import { eventPushSecrets } from '../../events'; import { EventService } from '../../services'; -import { getPostHogClient } from '../../services'; +import { TelemetryService } from '../../services'; interface PushSecret { ciphertextKey: string; @@ -38,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); let { secrets }: { secrets: PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -112,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => { let secrets; let key; try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; @@ -181,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { let secrets; let key; try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index cd91dca67..91328ea7b 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { AnyBulkWriteOperation } from 'mongodb'; import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; -import { getPostHogClient } from '../../services'; +import { TelemetryService } from '../../services'; /** * Create secret for workspace with id [workspaceId] and environment [environment] @@ -15,7 +15,7 @@ import { getPostHogClient } from '../../services'; * @param res */ export const createSecret = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const secretToCreate: CreateSecretRequestBody = req.body.secret; const { workspaceId, environment } = req.params const sanitizedSecret: SanitizedSecretForCreate = { @@ -68,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => { * @param res */ export const createSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const { workspaceId, environment } = req.params const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] @@ -130,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretIdsToDelete: string[] = req.body.secretIds @@ -184,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecret = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); await Secret.findByIdAndDelete(req._secret._id) if (postHogClient) { @@ -213,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => { * @returns */ export const updateSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; 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 */ export const updateSecret = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; @@ -335,7 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => { * @returns */ export const getSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { environment } = req.query; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 66aecd423..169a8a8cb 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -15,12 +15,12 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors'; import { EventService } from '../../services'; import { eventPushSecrets } from '../../events'; import { EESecretService, EELogService } from '../../ee/services'; -import { getPostHogClient } from '../../services'; +import { TelemetryService } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import Tag from '../../models/tag'; -import _ from 'lodash'; +import _, { eq } from 'lodash'; import { BatchSecretRequest, BatchSecret @@ -28,12 +28,13 @@ import { /** * Peform a batch of any specified CUD secret operations + * (used by dashboard) * @param req * @param res */ export const batchSecrets = async (req: Request, res: Response) => { const channel = getChannelFromUserAgent(req.headers['user-agent']); - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { workspaceId, @@ -91,7 +92,9 @@ export const batchSecrets = async (req: Request, res: Response) => { const addAction = await EELogService.createAction({ 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), secretIds: createdSecrets.map((n) => n._id) }) as IAction; @@ -328,14 +331,15 @@ export const createSecrets = async (req: Request, res: Response) => { } } */ - const postHogClient = getPostHogClient(); const channel = getChannelFromUserAgent(req.headers['user-agent']) const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body; - const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE) - if (!hasAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + if (req.user) { + const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, ABILITY_WRITE) + if (!hasAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } } let listOfSecretsToCreate; @@ -378,7 +382,7 @@ export const createSecrets = async (req: Request, res: Response) => { version: 1, workspace: new Types.ObjectId(workspaceId), type, - user: type === SECRET_PERSONAL ? req.user : undefined, + user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined, environment, secretKeyCiphertext, secretKeyIV, @@ -391,7 +395,7 @@ export const createSecrets = async (req: Request, res: Response) => { secretCommentTag, tags }); - }) + }); 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({ 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), secretIds: newlyCreatedSecrets.map((n) => n._id) }); // (EE) create (audit) log 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), actions: [addAction], channel, @@ -466,10 +474,15 @@ export const createSecrets = async (req: Request, res: Response) => { workspaceId }); + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets added', - distinctId: req.user.email, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: listOfSecretsToCreate.length, environment, @@ -533,91 +546,118 @@ 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(',') : []; - 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 to env and project - let hasWriteOnlyAccess - if (!req.serviceTokenData) { - hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment) - const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment) - if (hasNoAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } - } - let secrets: any - let secretQuery: any - if (tagNamesList != undefined && tagNamesList.length != 0) { - const workspaceFromDB = await Tag.find({ workspace: workspaceId }) - - const tagIds = _.map(tagNamesList, (tagName) => { + 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) { + // case: client authorization is via JWT - secretQuery = { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, - { user: { $exists: false } } - ], - tags: { $in: tagIds }, - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + let hasWriteOnlyAccess + if (!req.serviceTokenData) { + hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment) + const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment) + if (hasNoAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } } - } else { - secretQuery = { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, - { user: { $exists: false } } - ], - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + + let secretQuery: any; + if (tagNamesList != undefined && tagNamesList.length != 0) { + const workspaceFromDB = await Tag.find({ workspace: workspaceId }) + + const tagIds = _.map(tagNamesList, (tagName) => { + const tag = _.find(workspaceFromDB, { slug: tagName }); + return tag ? tag.id : null; + }); + + secretQuery = { + workspace: workspaceId, + environment, + $or: [ + { user: req.user._id }, + { user: { $exists: false } } + ], + tags: { $in: tagIds }, + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + } else { + secretQuery = { + workspace: workspaceId, + environment, + $or: [ + { user: req.user._id }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + } + + 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") + } else { + secrets = await Secret.find(secretQuery).populate("tags") } } - - if (hasWriteOnlyAccess) { - secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag") - } else { - 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 readAction = await EELogService.createAction({ 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), secretIds: secrets.map((n: any) => n._id) }); - + 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), actions: [readAction], channel, ipAddress: req.ip }); + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets pulled', - distinctId: userEmail, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: secrets.length, 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) * @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'; - // TODO: move type interface PatchSecret { id: string; secretKeyCiphertext: string; @@ -865,14 +850,18 @@ export const updateSecrets = async (req: Request, res: Response) => { const updateAction = await EELogService.createAction({ 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), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) }); // (EE) create (audit) log 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), actions: [updateAction], channel, @@ -884,10 +873,15 @@ export const updateSecrets = async (req: Request, res: Response) => { workspaceId: key }) + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets modified', - distinctId: req.user.email, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: workspaceSecretObj[key].length, 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 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 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({ 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), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) }); // (EE) create (audit) log 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), actions: [deleteAction], channel, @@ -1011,10 +1013,15 @@ export const deleteSecrets = async (req: Request, res: Response) => { workspaceId: key }) + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets deleted', - distinctId: req.user.email, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: workspaceSecretObj[key].length, environment: workspaceSecretObj[key][0].environment, diff --git a/backend/src/controllers/v2/serviceAccountsController.ts b/backend/src/controllers/v2/serviceAccountsController.ts index d950fdddb..822dbccaa 100644 --- a/backend/src/controllers/v2/serviceAccountsController.ts +++ b/backend/src/controllers/v2/serviceAccountsController.ts @@ -195,10 +195,8 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re const { environment, workspaceId, - canRead = false, - canWrite = false, - canUpdate = false, - canDelete = false, + read = false, + write = false, encryptedKey, nonce } = req.body; @@ -221,10 +219,8 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re serviceAccount: new Types.ObjectId(serviceAccountId), workspace: new Types.ObjectId(workspaceId), environment, - canRead, - canWrite, - canUpdate, - canDelete + read, + write }).save(); const existingServiceAccountKey = await ServiceAccountKey.findOne({ diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 367b66993..1d5ad9f60 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -19,7 +19,7 @@ import { reformatPullSecrets } from '../../helpers/secret'; import { pushKeys } from '../../helpers/key'; -import { getPostHogClient, EventService } from '../../services'; +import { TelemetryService, EventService } from '../../services'; import { eventPushSecrets } from '../../events'; interface V2PushSecret { @@ -48,7 +48,7 @@ interface V2PushSecret { export const pushWorkspaceSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -122,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => { let secrets; try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; diff --git a/backend/src/ee/helpers/action.ts b/backend/src/ee/helpers/action.ts index eb94810f4..33111d4ba 100644 --- a/backend/src/ee/helpers/action.ts +++ b/backend/src/ee/helpers/action.ts @@ -24,11 +24,15 @@ import { const createActionUpdateSecret = async ({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId: Types.ObjectId; secretIds: Types.ObjectId[]; }) => { @@ -46,6 +50,8 @@ const createActionUpdateSecret = async ({ action = await new Action({ name, user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, workspace: workspaceId, payload: { secretVersions: latestSecretVersions @@ -72,11 +78,15 @@ const createActionUpdateSecret = async ({ const createActionSecret = async ({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId: Types.ObjectId; secretIds: Types.ObjectId[]; }) => { @@ -94,6 +104,8 @@ const createActionSecret = async ({ action = await new Action({ name, user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, workspace: workspaceId, payload: { 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 {String} obj.name - name of action * @param {String} obj.userId - id of user associated with action * @returns */ -const createActionUser = ({ +const createActionClient = ({ name, - userId + userId, + serviceAccountId, + serviceTokenDataId }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; }) => { let action; try { action = new Action({ name, - user: userId + user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId }).save(); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to create user action'); + throw new Error('Failed to create client action'); } return action; @@ -149,11 +168,15 @@ const createActionUser = ({ const createActionHelper = async ({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds, }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; secretIds?: Types.ObjectId[]; }) => { @@ -162,7 +185,7 @@ const createActionHelper = async ({ switch (name) { case ACTION_LOGIN: case ACTION_LOGOUT: - action = await createActionUser({ + action = await createActionClient({ name, userId }); diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts index 50cd28917..d965c31e0 100644 --- a/backend/src/ee/helpers/checkMembershipPermissions.ts +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -1,8 +1,9 @@ +import { Types } from 'mongoose'; import _ from "lodash"; import { Membership } from "../../models"; 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 }) if (!membershipForWorkspace) { return false @@ -18,7 +19,7 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi 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 }) if (!membershipForWorkspace) { return false @@ -36,7 +37,7 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env 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 }) if (!membershipForWorkspace) { return true diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts index bdb0a2380..77d280ee1 100644 --- a/backend/src/ee/helpers/log.ts +++ b/backend/src/ee/helpers/log.ts @@ -16,12 +16,16 @@ import { */ const createLogHelper = async ({ userId, + serviceAccountId, + serviceTokenDataId, workspaceId, actions, channel, ipAddress }: { - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; actions: IAction[]; channel: string; @@ -31,6 +35,8 @@ const createLogHelper = async ({ try { log = await new Log({ user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, workspace: workspaceId ?? undefined, actionNames: actions.map((a) => a.name), actions, diff --git a/backend/src/ee/models/action.ts b/backend/src/ee/models/action.ts index 7cdd7a355..055e144fd 100644 --- a/backend/src/ee/models/action.ts +++ b/backend/src/ee/models/action.ts @@ -11,6 +11,8 @@ import { export interface IAction { name: string; user?: Types.ObjectId, + serviceAccount?: Types.ObjectId, + serviceTokenData?: Types.ObjectId, workspace?: Types.ObjectId, payload?: { secretVersions?: Types.ObjectId[] @@ -33,8 +35,15 @@ const actionSchema = new Schema( }, user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: 'User' + }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount' + }, + serviceTokenData: { + type: Schema.Types.ObjectId, + ref: 'ServiceTokenData' }, workspace: { type: Schema.Types.ObjectId, diff --git a/backend/src/ee/models/log.ts b/backend/src/ee/models/log.ts index 47be2e58f..9ed552640 100644 --- a/backend/src/ee/models/log.ts +++ b/backend/src/ee/models/log.ts @@ -11,6 +11,8 @@ import { export interface ILog { _id: Types.ObjectId; user?: Types.ObjectId; + serviceAccount?: Types.ObjectId; + serviceTokenData?: Types.ObjectId; workspace?: Types.ObjectId; actionNames: string[]; actions: Types.ObjectId[]; @@ -24,6 +26,14 @@ const logSchema = new Schema( type: Schema.Types.ObjectId, ref: 'User' }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount' + }, + serviceTokenData: { + type: Schema.Types.ObjectId, + ref: 'ServiceTokenData' + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace' diff --git a/backend/src/ee/services/EELogService.ts b/backend/src/ee/services/EELogService.ts index bbe03e09e..81d26765f 100644 --- a/backend/src/ee/services/EELogService.ts +++ b/backend/src/ee/services/EELogService.ts @@ -26,12 +26,16 @@ class EELogService { */ static async createLog({ userId, + serviceAccountId, + serviceTokenDataId, workspaceId, actions, channel, ipAddress }: { - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; actions: IAction[]; channel: string; @@ -40,6 +44,8 @@ class EELogService { if (!EELicenseService.isLicenseValid) return null; return await createLogHelper({ userId, + serviceAccountId, + serviceTokenDataId, workspaceId, actions, channel, @@ -59,17 +65,23 @@ class EELogService { static async createAction({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; secretIds?: Types.ObjectId[]; }) { return await createActionHelper({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 33d079fd4..58d338578 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -22,6 +22,12 @@ import { getJwtRefreshLifetime, getJwtRefreshSecret } 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 authHeader = headers['authorization']; - let authTokenType, authTokenValue; + let authMode, authTokenValue; if (apiKey === undefined && authHeader === undefined) { // case: no auth or X-API-KEY header present throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' }); @@ -47,7 +53,7 @@ const validateAuthMode = ({ if (typeof apiKey === 'string') { // case: treat request authentication type as via X-API-KEY (i.e. API Key) - authTokenType = 'apiKey'; + authMode = AUTH_MODE_API_KEY; authTokenValue = apiKey; } @@ -63,24 +69,24 @@ const validateAuthMode = ({ switch (tokenValue.split('.', 1)[0]) { case 'st': - authTokenType = 'serviceToken'; + authMode = AUTH_MODE_SERVICE_TOKEN; break; case 'sa': - authTokenType = 'serviceAccount'; + authMode = AUTH_MODE_SERVICE_ACCOUNT; break; default: - authTokenType = 'jwt'; + authMode = AUTH_MODE_JWT; } 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 ({ - authTokenType, + authMode, authTokenValue }); } @@ -155,8 +161,7 @@ const getAuthSTDPayload = async ({ serviceTokenData = await ServiceTokenData .findById(TOKEN_IDENTIFIER) - .select('+encryptedKey +iv +tag') - .populate<{user: IUser}>('user'); + .select('+encryptedKey +iv +tag'); if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); @@ -216,7 +221,7 @@ const getAuthAPIKeyPayload = async ({ const apiKeyData = await APIKeyData .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') - .populate('user', '+publicKey'); + .populate<{user: IUser}>('user', '+publicKey'); if (!apiKeyData) { throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index c0f57f3ef..57fa6ebda 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -24,57 +24,6 @@ import _ from 'lodash'; import { ABILITY_WRITE } from '../variables/organization'; 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 { ciphertextKey: string; ivKey: string; @@ -714,7 +663,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { }; export { - validateSecrets, v1PushSecrets, v2PushSecrets, pullSecrets, diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts new file mode 100644 index 000000000..11f19499e --- /dev/null +++ b/backend/src/helpers/secrets.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/helpers/serviceAccount.ts b/backend/src/helpers/serviceAccount.ts index e69de29bb..fca64e8fd 100644 --- a/backend/src/helpers/serviceAccount.ts +++ b/backend/src/helpers/serviceAccount.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/helpers/serviceTokenData.ts b/backend/src/helpers/serviceTokenData.ts new file mode 100644 index 000000000..70c9d416b --- /dev/null +++ b/backend/src/helpers/serviceTokenData.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/helpers/telemetry.ts b/backend/src/helpers/telemetry.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 932a4bd81..668635f91 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,6 +1,18 @@ 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 { validateMembership } from './membership'; +import _ from 'lodash'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { + ABILITY_WRITE +} from '../variables/organization'; /** * 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 +}; diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index f7cdd4138..0dd471004 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -5,47 +5,98 @@ import { Bot, Membership, Key, - Secret + Secret, + User, + IUser, + ServiceAccountWorkspacePermission, + ServiceAccount, + IServiceAccount, + ServiceTokenData, + IServiceTokenData, } 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 } 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], - * and [serviceTokenDataId] for workspace with id [workspaceId] based + * Validate accepted clients for workspace with id [workspaceId] based * on any known permissions. * @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 ({ - userId, - serviceAccountId, - serviceTokenDataId, + authData, workspaceId, - environment + environment, + requiredPermissions }: { - userId?: Types.ObjectId; - serviceAccountId?: Types.ObjectId; - serviceTokenDataId?: Types.ObjectId; + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, workspaceId: Types.ObjectId; environment?: string; + requiredPermissions?: string[]; }) => { - + let membership; - if (userId) { - membership = await validateMembership({ - userId, - workspaceId + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + membership = await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId, + environment, + requiredPermissions }); + // TODO: validate user against [requiredPermissions] } - if (serviceAccountId) { - // TODO + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + 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) { - // TODO + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + 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 ({ diff --git a/backend/src/index.ts b/backend/src/index.ts index 955fc96b1..56fcc288a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node'; import { DatabaseService } from './services'; import { setUpHealthEndpoint } from './services/health'; import { initSmtp } from './services/smtp'; -import { logTelemetryMessage } from './services'; +import { TelemetryService } from './services'; import { setTransporter } from './helpers/nodemailer'; import { createTestUserForDevelopment } from './utils/addDevelopmentUser'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -80,7 +80,7 @@ const main = async () => { }); } - logTelemetryMessage(); + TelemetryService.logTelemetryMessage(); setTransporter(initSmtp()); await DatabaseService.initDatabase(getMongoURL()); diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index e9f777e0b..f55e60676 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -10,6 +10,17 @@ import { import { UnauthorizedRequestError } 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' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -28,59 +39,51 @@ declare module 'jsonwebtoken' { * @returns */ const requireAuth = ({ - acceptedAuthModes = ['jwt'], - requiredServiceTokenPermissions = [] + acceptedAuthModes = [AUTH_MODE_JWT], }: { acceptedAuthModes: string[]; - requiredServiceTokenPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { // validate auth token against accepted auth modes [acceptedAuthModes] // and return token type [authTokenType] and value [authTokenValue] - const { authTokenType, authTokenValue } = validateAuthMode({ + const { authMode, authTokenValue } = validateAuthMode({ headers: req.headers, acceptedAuthModes }); - req.authTokenType = authTokenType; - - // attach auth payloads - let serviceTokenData: any; - switch (authTokenType) { - case 'serviceAccount': - req.serviceAccount = await getAuthSAAKPayload({ + let authPayload: IUser | IServiceAccount | IServiceTokenData; + switch (authMode) { + case AUTH_MODE_SERVICE_ACCOUNT: + authPayload = await getAuthSAAKPayload({ authTokenValue }); + req.serviceAccount = authPayload; break; - case 'serviceToken': - serviceTokenData = await getAuthSTDPayload({ + case AUTH_MODE_SERVICE_TOKEN: + authPayload = await getAuthSTDPayload({ authTokenValue }); - - // 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; - + req.serviceTokenData = authPayload; break; - case 'apiKey': - // TODO: deprecate API key - req.user = await getAuthAPIKeyPayload({ + case AUTH_MODE_API_KEY: + authPayload = await getAuthAPIKeyPayload({ authTokenValue }); + req.user = authPayload; break; default: - req.user = await getAuthUserPayload({ + authPayload = await getAuthUserPayload({ authTokenValue }); + req.user = authPayload; break; } + req.authData = { + authMode, + authPayload + } + return next(); } } diff --git a/backend/src/middleware/requireSecretsAuth.ts b/backend/src/middleware/requireSecretsAuth.ts index 2a6c36056..5a550156a 100644 --- a/backend/src/middleware/requireSecretsAuth.ts +++ b/backend/src/middleware/requireSecretsAuth.ts @@ -1,48 +1,34 @@ import { Request, Response, NextFunction } from 'express'; import { UnauthorizedRequestError } from '../utils/errors'; import { Secret, Membership } from '../models'; -import { validateSecrets } from '../helpers/secret'; - -// TODO: make this work for delete route +import { validateClientForSecrets } from '../helpers/secrets'; const requireSecretsAuth = ({ - acceptedRoles + acceptedRoles, + requiredPermissions = [] }: { acceptedRoles: string[]; + requiredPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - let secrets; - try { - if (Array.isArray(req.body.secrets)) { - // case: validate multiple secrets - secrets = await validateSecrets({ - userId: req.user._id.toString(), - 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)) { - secrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds: req.body.secretIds - }); - } else if (typeof req.body.secretIds === 'string') { - // case: validate secretIds - secrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds: [req.body.secretIds] - }); - } - - req.secrets = secrets; - return next(); - } catch (err) { - return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' })); + let secretIds = []; + if (Array.isArray(req.body.secrets)) { + secretIds = req.body.secrets.map((s: any) => s.id); + } else if (typeof req.body.secrets === 'object') { + secretIds = [req.body.secrets.id]; + } else if (Array.isArray(req.body.secretIds)) { + secretIds = req.body.secretIds; + } else if (typeof req.body.secretIds === 'string') { + secretIds = [req.body.secretIds]; } + + req.secrets = await validateClientForSecrets({ + authData: req.authData, + secretIds: [req.body.secretIds], + requiredPermissions + }); + + return next(); } } diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index a96625182..31b02b20b 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -16,44 +16,32 @@ type req = 'params' | 'body' | 'query'; const requireWorkspaceAuth = ({ acceptedRoles, locationWorkspaceId, - locationEnvironment = undefined + locationEnvironment = undefined, + requiredPermissions = [] }: { acceptedRoles: string[]; locationWorkspaceId: req; locationEnvironment?: req | undefined; + requiredPermissions?: string[]; }) => { 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 environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; - - // validate clients - const { membership } = await validateClientForWorkspace({ - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(workspaceId), - environment - }); - - if (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(); - } catch (err) { - return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) + const workspaceId = req[locationWorkspaceId]?.workspaceId; + const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; + + // validate clients + const { membership } = await validateClientForWorkspace({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + requiredPermissions + }); + + if (membership) { + req.membership = membership; } + + return next(); }; }; diff --git a/backend/src/models/serviceAccountWorkspacePermission.ts b/backend/src/models/serviceAccountWorkspacePermission.ts index 6e9a4d86f..01e4c4ba6 100644 --- a/backend/src/models/serviceAccountWorkspacePermission.ts +++ b/backend/src/models/serviceAccountWorkspacePermission.ts @@ -5,10 +5,8 @@ export interface IServiceAccountWorkspacePermission extends Document { serviceAccount: Types.ObjectId; workspace: Types.ObjectId; environment: string; - canRead: boolean; - canWrite: boolean; - canUpdate: boolean; - canDelete: boolean; + read: boolean; + write: boolean; } const serviceAccountWorkspacePermissionSchema = new Schema( @@ -27,19 +25,11 @@ const serviceAccountWorkspacePermissionSchema = new Schema secretId !== undefined) if (secretIds.length > 0) { - const relevantSecrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds + req.secrets = await validateClientForSecrets({ + authData: req.authData, + secretIds, + requiredPermissions: [] }); - - req.secrets = relevantSecrets; } } return true; @@ -100,12 +100,13 @@ router.post( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['write'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.createSecrets ); @@ -117,12 +118,13 @@ router.get( query('tagSlugs'), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['read'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'query' + locationWorkspaceId: 'query', + locationEnvironment: 'query', + requiredPermissions: [PERMISSION_READ_SECRETS] }), secretsController.getSecrets ); @@ -157,11 +159,11 @@ router.patch( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['write'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount'] }), requireSecretsAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.updateSecrets ); @@ -186,11 +188,11 @@ router.delete( .isEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['write'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount'] }), requireSecretsAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.deleteSecrets ); diff --git a/backend/src/routes/v2/serviceAccounts.ts b/backend/src/routes/v2/serviceAccounts.ts index 1b93a65c6..397c98d12 100644 --- a/backend/src/routes/v2/serviceAccounts.ts +++ b/backend/src/routes/v2/serviceAccounts.ts @@ -125,10 +125,8 @@ router.post( param('serviceAccountId').exists().isString().trim(), body('workspaceId').exists().isString().notEmpty(), body('environment').exists().isString().notEmpty(), - body('canRead').isBoolean().optional(), - body('canWrite').isBoolean().optional(), - body('canUpdate').isBoolean().optional(), - body('canDelete').isBoolean().optional(), + body('read').isBoolean().optional(), + body('write').isBoolean().optional(), body('encryptedKey').exists().isString().notEmpty(), body('nonce').exists().isString().notEmpty(), validateRequest, diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index d78e1e0f1..271381502 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -5,7 +5,7 @@ import { tagController } from '../../controllers/v2'; import { requireAuth, requireWorkspaceAuth, - validateRequest, + validateRequest } from '../../middleware'; import { ADMIN, MEMBER } from '../../variables'; @@ -16,6 +16,7 @@ router.get( }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -39,6 +40,7 @@ router.post( }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('name').exists().trim(), diff --git a/backend/src/services/PostHogClient.ts b/backend/src/services/PostHogClient.ts deleted file mode 100644 index 15ccf0919..000000000 --- a/backend/src/services/PostHogClient.ts +++ /dev/null @@ -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 -} - diff --git a/backend/src/services/TelemetryService.ts b/backend/src/services/TelemetryService.ts new file mode 100644 index 000000000..0566439b4 --- /dev/null +++ b/backend/src/services/TelemetryService.ts @@ -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; \ No newline at end of file diff --git a/backend/src/services/index.ts b/backend/src/services/index.ts index d98b70718..f93f45f58 100644 --- a/backend/src/services/index.ts +++ b/backend/src/services/index.ts @@ -1,13 +1,15 @@ import DatabaseService from './DatabaseService'; -import { logTelemetryMessage, getPostHogClient } from './PostHogClient'; +// import { logTelemetryMessage, getPostHogClient } from './TelemetryService'; +import TelemetryService from './TelemetryService'; import BotService from './BotService'; import EventService from './EventService'; import IntegrationService from './IntegrationService'; import TokenService from './TokenService'; export { - logTelemetryMessage, - getPostHogClient, + TelemetryService, + // logTelemetryMessage, + // getPostHogClient, DatabaseService, BotService, EventService, diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index a64a16f3d..89604e177 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -24,7 +24,7 @@ declare global { serviceTokenData: any; apiKeyData: any; query?: any; - authTokenType: string; + authData: any; } } } diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts new file mode 100644 index 000000000..2fe1f4fc6 --- /dev/null +++ b/backend/src/variables/authentication.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index b71044cba..1e2ed4ae9 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -63,6 +63,16 @@ import { TOKEN_EMAIL_ORG_INVITATION, TOKEN_EMAIL_PASSWORD_RESET } from './token'; +import { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from './permission'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from './authentication'; export { OWNER, @@ -113,6 +123,8 @@ export { ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, ACTION_READ_SECRETS, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, getIntegrationOptions, SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN, @@ -124,5 +136,9 @@ export { TOKEN_EMAIL_CONFIRMATION, TOKEN_EMAIL_MFA, 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 }; diff --git a/backend/src/variables/permission.ts b/backend/src/variables/permission.ts new file mode 100644 index 000000000..769344d7f --- /dev/null +++ b/backend/src/variables/permission.ts @@ -0,0 +1,7 @@ +const PERMISSION_READ_SECRETS = 'read'; +const PERMISSION_WRITE_SECRETS = 'write'; + +export { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} \ No newline at end of file diff --git a/frontend/src/hooks/api/serviceAccounts/types.ts b/frontend/src/hooks/api/serviceAccounts/types.ts index 5d4981db2..f8865729b 100644 --- a/frontend/src/hooks/api/serviceAccounts/types.ts +++ b/frontend/src/hooks/api/serviceAccounts/types.ts @@ -31,20 +31,16 @@ export type ServiceAccountWorkspacePermission = { serviceAccount: string; workspace: Workspace; environment: string; - canRead: boolean; - canWrite: boolean; - canUpdate: boolean; - canDelete: boolean; + read: boolean; + write: boolean; } export type CreateServiceAccountWorkspacePermissionDTO = { serviceAccountId: string; workspaceId: string; environment: string; - canRead: boolean; - canWrite: boolean; - canUpdate: boolean; - canDelete: boolean; + read: boolean; + write: boolean; encryptedKey: string; nonce: string; } diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx index b2319c9cc..9ce8ab406 100644 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx +++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx @@ -48,10 +48,8 @@ const createProjectLevelPermissionSchema = yup.object({ workspace: yup.string().required().label('Workspace'), environment: yup.string().required().label('Environment'), permissions: yup.object().shape({ - canRead: yup.boolean().required(), - canWrite: yup.boolean().required(), - canUpdate: yup.boolean().required(), - canDelete: yup.boolean().required(), + read: yup.boolean().required(), + write: yup.boolean().required() }).defined().required() }); @@ -91,7 +89,7 @@ export const SAProjectLevelPermissionsTable = ({ privateKey, workspace, environment, - permissions: { canRead, canWrite, canUpdate, canDelete } + permissions: { read, write } }: CreateProjectLevelPermissionForm) => { // TODO: clean up / modularize this function @@ -126,10 +124,8 @@ export const SAProjectLevelPermissionsTable = ({ serviceAccountId, workspaceId: workspace, environment, - canRead, - canWrite, - canUpdate, - canDelete, + read, + write, encryptedKey: ciphertext, nonce }); @@ -187,10 +183,8 @@ export const SAProjectLevelPermissionsTable = ({ _id, workspace, environment, - canRead, - canWrite, - canUpdate, - canDelete + read, + write }) => { const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name; return ( @@ -200,28 +194,14 @@ export const SAProjectLevelPermissionsTable = ({ {/**/} {/**/} - - - {/**/} - - - {/**/} @@ -352,28 +332,18 @@ export const SAProjectLevelPermissionsTable = ({ control={control} name="permissions" defaultValue={{ - canRead: true, - canWrite: false, - canUpdate: false, - canDelete: false + read: true, + write: false }} render={({ field: { onChange, value }, fieldState: { error }}) => { const options = [ { label: 'Read (default)', - value: 'canRead' + value: 'read' }, { label: 'Write', - value: 'canWrite' - }, - { - label: 'Update', - value: 'canUpdate' - }, - { - label: 'Delete', - value: 'canDelete' + value: 'write' } ];