diff --git a/backend/package-lock.json b/backend/package-lock.json index 524a6017b..0a4a2db17 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -71,6 +71,7 @@ "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", + "@types/picomatch": "^2.3.0", "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", @@ -2200,6 +2201,26 @@ } } }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/schemas": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", @@ -3241,6 +3262,12 @@ "@types/express": "*" } }, + "node_modules/@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "node_modules/@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -5680,25 +5707,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6481,6 +6489,26 @@ } } }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-diff": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", @@ -6776,6 +6804,26 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-snapshot": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", @@ -11071,6 +11119,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -11669,6 +11736,25 @@ "node": ">=0.4.0" } }, + "node_modules/swagger-autogen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/swagger-ui-dist": { "version": "4.19.0", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.0.tgz", @@ -11723,6 +11809,26 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -14154,6 +14260,22 @@ "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "@jest/schemas": { @@ -14988,6 +15110,12 @@ "@types/express": "*" } }, + "@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -16808,19 +16936,6 @@ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -17371,6 +17486,22 @@ "pretty-format": "^29.5.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "jest-diff": { @@ -17606,6 +17737,22 @@ "jest-util": "^29.5.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "jest-snapshot": { @@ -20674,6 +20821,21 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "ripemd160": { @@ -21129,6 +21291,19 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } } } }, @@ -21174,6 +21349,22 @@ "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "text-hex": { diff --git a/backend/package.json b/backend/package.json index 448ccfac2..0c0f0642d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -89,6 +89,7 @@ "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", + "@types/picomatch": "^2.3.0", "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index a760a511c..8307d373d 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,5 +1,6 @@ import { Types } from "mongoose"; import { Request, Response } from "express"; +import picomatch from "picomatch"; import { ISecret, Secret, ServiceTokenData } from "../../models"; import { IAction, SecretVersion } from "../../ee/models"; import { @@ -9,7 +10,7 @@ import { ACTION_UPDATE_SECRETS, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, - SECRET_PERSONAL, + SECRET_PERSONAL } from "../../variables"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { EventService } from "../../services"; @@ -21,7 +22,7 @@ import { PERMISSION_WRITE_SECRETS } from "../../variables"; import { userHasNoAbility, userHasWorkspaceAccess, - userHasWriteOnlyAbility, + userHasWriteOnlyAbility } from "../../ee/helpers/checkMembershipPermissions"; import Tag from "../../models/tag"; import _ from "lodash"; @@ -30,7 +31,7 @@ import Folder from "../../models/folder"; import { getFolderByPath, getFolderIdFromServiceToken, - searchByFolderId, + searchByFolderId } from "../../services/FolderService"; /** @@ -47,7 +48,7 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, environment, requests, - secretPath, + secretPath }: { workspaceId: string; environment: string; @@ -63,7 +64,7 @@ export const batchSecrets = async (req: Request, res: Response) => { // get secret blind index salt const salt = await SecretService.getSecretBlindIndexSalt({ - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); const folders = await Folder.findOne({ workspace: workspaceId, environment }); @@ -73,22 +74,22 @@ export const batchSecrets = async (req: Request, res: Response) => { } if (req.authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; + const { scopes: tkScopes } = req.authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); + // in service token when not giving secretpath folderid must be root // this is to avoid giving folderid when service tokens are used - if ( - (!secretPath && folderId !== "root") || - (secretPath && secretPath !== serviceTkScopedSecretPath) - ) { + if ((!secretPath && folderId !== "root") || (secretPath && !validScope)) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } + if (secretPath) { - folderId = await getFolderIdFromServiceToken( - workspaceId, - environment, - secretPath - ); + folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); } for await (const request of requests) { @@ -97,12 +98,10 @@ export const batchSecrets = async (req: Request, res: Response) => { let secretBlindIndex = ""; switch (request.method) { case "POST": - secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( - { - secretName: request.secret.secretName, - salt, - } - ); + secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ + secretName: request.secret.secretName, + salt + }); createSecrets.push({ ...request.secret, @@ -113,16 +112,14 @@ export const batchSecrets = async (req: Request, res: Response) => { folder: folderId, secretBlindIndex, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }); break; case "PATCH": - secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( - { - secretName: request.secret.secretName, - salt, - } - ); + secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ + secretName: request.secret.secretName, + salt + }); updateSecrets.push({ ...request.secret, @@ -130,7 +127,7 @@ export const batchSecrets = async (req: Request, res: Response) => { secretBlindIndex, folder: folderId, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }); break; case "DELETE": @@ -150,9 +147,9 @@ export const batchSecrets = async (req: Request, res: Response) => { ...n._doc, _id: new Types.ObjectId(), secret: n._id, - isDeleted: false, + isDeleted: false }; - }), + }) }); const addAction = (await EELogService.createAction({ @@ -161,7 +158,7 @@ export const batchSecrets = async (req: Request, res: Response) => { serviceAccountId: req.serviceAccount?._id, serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId), - secretIds: createdSecrets.map((n) => n._id), + secretIds: createdSecrets.map((n) => n._id) })) as IAction; actions.push(addAction); @@ -175,8 +172,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, folderId, channel, - userAgent: req.headers?.["user-agent"], - }, + userAgent: req.headers?.["user-agent"] + } }); } } @@ -195,7 +192,7 @@ export const batchSecrets = async (req: Request, res: Response) => { listedSecretsObj = req.secrets.reduce( (obj: any, secret: ISecret) => ({ ...obj, - [secret._id.toString()]: secret, + [secret._id.toString()]: secret }), {} ); @@ -204,16 +201,16 @@ export const batchSecrets = async (req: Request, res: Response) => { updateOne: { filter: { _id: new Types.ObjectId(u._id), - workspace: new Types.ObjectId(workspaceId), + workspace: new Types.ObjectId(workspaceId) }, update: { $inc: { - version: 1, + version: 1 }, ...u, - _id: new Types.ObjectId(u._id), - }, - }, + _id: new Types.ObjectId(u._id) + } + } })); await Secret.bulkWrite(updateOperations); @@ -240,25 +237,25 @@ export const batchSecrets = async (req: Request, res: Response) => { algorithm: ALGORITHM_AES_256_GCM, keyEncoding: ENCODING_SCHEME_UTF8, tags: u.tags, - folder: u.folder, + folder: u.folder }) ); await EESecretService.addSecretVersions({ - secretVersions, + secretVersions }); updatedSecrets = await Secret.find({ _id: { - $in: updateSecrets.map((u) => new Types.ObjectId(u._id)), - }, + $in: updateSecrets.map((u) => new Types.ObjectId(u._id)) + } }); const updateAction = (await EELogService.createAction({ name: ACTION_UPDATE_SECRETS, userId: req.user._id, workspaceId: new Types.ObjectId(workspaceId), - secretIds: updatedSecrets.map((u) => u._id), + secretIds: updatedSecrets.map((u) => u._id) })) as IAction; actions.push(updateAction); @@ -272,8 +269,8 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId, folderId, channel, - userAgent: req.headers?.["user-agent"], - }, + userAgent: req.headers?.["user-agent"] + } }); } } @@ -282,19 +279,19 @@ export const batchSecrets = async (req: Request, res: Response) => { if (deleteSecrets.length > 0) { await Secret.deleteMany({ _id: { - $in: deleteSecrets, - }, + $in: deleteSecrets + } }); await EESecretService.markDeletedSecretVersions({ - secretIds: deleteSecrets, + secretIds: deleteSecrets }); const deleteAction = (await EELogService.createAction({ name: ACTION_DELETE_SECRETS, userId: req.user._id, workspaceId: new Types.ObjectId(workspaceId), - secretIds: deleteSecrets, + secretIds: deleteSecrets })) as IAction; actions.push(deleteAction); @@ -307,8 +304,8 @@ export const batchSecrets = async (req: Request, res: Response) => { environment, workspaceId, channel: channel, - userAgent: req.headers?.["user-agent"], - }, + userAgent: req.headers?.["user-agent"] + } }); } } @@ -320,22 +317,22 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), actions, channel, - ipAddress: req.realIP, + ipAddress: req.realIP }); } // // trigger event - push secrets await EventService.handleEvent({ event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - }), + workspaceId: new Types.ObjectId(workspaceId) + }) }); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, + folderId }); const resObj: { [key: string]: ISecret[] | string[] } = {}; @@ -418,7 +415,7 @@ export const createSecrets = async (req: Request, res: Response) => { const { workspaceId, environment, - secretPath, + secretPath }: { workspaceId: string; environment: string; @@ -435,8 +432,7 @@ export const createSecrets = async (req: Request, res: Response) => { ); if (!hasAccess) { throw UnauthorizedRequestError({ - message: - "You do not have the necessary permission(s) perform this action", + message: "You do not have the necessary permission(s) perform this action" }); } } @@ -449,28 +445,28 @@ export const createSecrets = async (req: Request, res: Response) => { // case: create 1 secret listOfSecretsToCreate = [req.body.secrets]; } + if (req.authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; + const { scopes: tkScopes } = req.authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch(secretPath || "/", scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); + // in service token when not giving secretpath folderid must be root // this is to avoid giving folderid when service tokens are used - if ( - (!secretPath && folderId !== "root") || - (secretPath && secretPath !== serviceTkScopedSecretPath) - ) { + if ((!secretPath && folderId !== "root") || (secretPath && !validScope)) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } if (secretPath) { - folderId = await getFolderIdFromServiceToken( - workspaceId, - environment, - secretPath - ); + folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); } // get secret blind index salt const salt = await SecretService.getSecretBlindIndexSalt({ - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); type secretsToCreateType = { @@ -502,15 +498,14 @@ export const createSecrets = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, - tags, + tags }: secretsToCreateType) => { let secretBlindIndex; if (secretName) { - secretBlindIndex = - await SecretService.generateSecretBlindIndexWithSalt({ - secretName, - salt, - }); + secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ + secretName, + salt + }); } return { @@ -532,22 +527,22 @@ export const createSecrets = async (req: Request, res: Response) => { secretCommentTag, algorithm: ALGORITHM_AES_256_GCM, keyEncoding: ENCODING_SCHEME_UTF8, - tags, + tags }; } ) ); - const newlyCreatedSecrets: ISecret[] = ( - await Secret.insertMany(secretsToInsert) - ).map((insertedSecret) => insertedSecret.toObject()); + const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map( + (insertedSecret) => insertedSecret.toObject() + ); setTimeout(async () => { // trigger event - push secrets await EventService.handleEvent({ event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - }), + workspaceId: new Types.ObjectId(workspaceId) + }) }); }, 5000); @@ -567,7 +562,7 @@ export const createSecrets = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag, + secretValueTag }) => new SecretVersion({ secret: _id, @@ -586,9 +581,9 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueTag, folder: folderId, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }) - ), + ) }); const addAction = await EELogService.createAction({ @@ -597,7 +592,7 @@ export const createSecrets = async (req: Request, res: Response) => { serviceAccountId: req.serviceAccount?._id, serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId), - secretIds: newlyCreatedSecrets.map((n) => n._id), + secretIds: newlyCreatedSecrets.map((n) => n._id) }); // (EE) create (audit) log @@ -609,14 +604,14 @@ export const createSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), actions: [addAction], channel, - ipAddress: req.realIP, + ipAddress: req.realIP })); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, + folderId }); const postHogClient = await TelemetryService.getPostHogClient(); @@ -624,7 +619,7 @@ export const createSecrets = async (req: Request, res: Response) => { postHogClient.capture({ event: "secrets added", distinctId: await TelemetryService.getDistinctId({ - authData: req.authData, + authData: req.authData }), properties: { numberOfSecrets: listOfSecretsToCreate.length, @@ -632,13 +627,13 @@ export const createSecrets = async (req: Request, res: Response) => { workspaceId, channel: channel, folderId, - userAgent: req.headers?.["user-agent"], - }, + userAgent: req.headers?.["user-agent"] + } }); } return res.status(200).send({ - secrets: newlyCreatedSecrets, + secrets: newlyCreatedSecrets }); }; @@ -696,10 +691,7 @@ export const getSecrets = async (req: Request, res: Response) => { const environment = req.query.environment as string; const folders = await Folder.findOne({ workspace: workspaceId, environment }); - if ( - (!folders && folderId && folderId !== "root") || - (!folders && secretPath) - ) { + if ((!folders && folderId && folderId !== "root") || (!folders && secretPath)) { res.send({ secrets: [] }); return; } @@ -712,13 +704,17 @@ export const getSecrets = async (req: Request, res: Response) => { } if (req.authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; + const { scopes: tkScopes } = req.authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch((secretPath as string) || "/", scope.secretPath, { + strictSlashes: false + }) && scope.environment === environment + ); + // in service token when not giving secretpath folderid must be root // this is to avoid giving folderid when service tokens are used - if ( - (!secretPath && folderId !== "root") || - (secretPath && secretPath !== serviceTkScopedSecretPath) - ) { + if ((!secretPath && folderId !== "root") || (secretPath && !validScope)) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } @@ -738,8 +734,7 @@ export const getSecrets = async (req: Request, res: Response) => { // query tags table to get all tags ids for the tag names for the given workspace let tagIds = []; - const tagNamesList = - typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : []; + const tagNamesList = typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : []; if (tagNamesList != undefined && tagNamesList.length != 0) { const workspaceFromDB = await Tag.find({ workspace: workspaceId }); tagIds = _.map(tagNamesList, (tagName: string) => { @@ -762,8 +757,7 @@ export const getSecrets = async (req: Request, res: Response) => { ); if (hasNoAccess) { throw UnauthorizedRequestError({ - message: - "You do not have the necessary permission(s) perform this action", + message: "You do not have the necessary permission(s) perform this action" }); } @@ -773,8 +767,8 @@ export const getSecrets = async (req: Request, res: Response) => { folder: folderId, $or: [ { user: req.user._id }, // personal secrets for this user - { user: { $exists: false } }, // shared secrets from workspace - ], + { user: { $exists: false } } // shared secrets from workspace + ] }; if (tagIds.length > 0) { @@ -801,8 +795,8 @@ export const getSecrets = async (req: Request, res: Response) => { environment, $or: [ { user: userId }, // personal secrets for this user - { user: { $exists: false } }, // shared secrets from workspace - ], + { user: { $exists: false } } // shared secrets from workspace + ] }; if (tagIds.length > 0) { @@ -820,7 +814,7 @@ export const getSecrets = async (req: Request, res: Response) => { workspace: workspaceId, environment, folder: folderId, - user: { $exists: false }, // shared secrets only from workspace + user: { $exists: false } // shared secrets only from workspace }; if (tagIds.length > 0) { @@ -838,7 +832,7 @@ export const getSecrets = async (req: Request, res: Response) => { serviceAccountId: req.serviceAccount?._id, serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId as string), - secretIds: secrets.map((n: any) => n._id), + secretIds: secrets.map((n: any) => n._id) }); readAction && @@ -849,7 +843,7 @@ export const getSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId as string), actions: [readAction], channel, - ipAddress: req.realIP, + ipAddress: req.realIP })); const postHogClient = await TelemetryService.getPostHogClient(); @@ -857,7 +851,7 @@ export const getSecrets = async (req: Request, res: Response) => { postHogClient.capture({ event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ - authData: req.authData, + authData: req.authData }), properties: { numberOfSecrets: secrets.length, @@ -865,13 +859,13 @@ export const getSecrets = async (req: Request, res: Response) => { workspaceId, channel, folderId, - userAgent: req.headers?.["user-agent"], - }, + userAgent: req.headers?.["user-agent"] + } }); } return res.status(200).send({ - secrets, + secrets }); }; @@ -925,9 +919,7 @@ export const updateSecrets = async (req: Request, res: Response) => { } } */ - const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") - ? "web" - : "cli"; + const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli"; interface PatchSecret { id: string; @@ -943,51 +935,47 @@ export const updateSecrets = async (req: Request, res: Response) => { tags: string[]; } - const updateOperationsToPerform = req.body.secrets.map( - (secret: PatchSecret) => { - const { - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - tags, - } = secret; + const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => { + const { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + tags + } = secret; - return { - updateOne: { - filter: { _id: new Types.ObjectId(secret.id) }, - update: { - $inc: { - version: 1, - }, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - tags, - ...(secretCommentCiphertext !== undefined && - secretCommentIV && - secretCommentTag - ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - } - : {}), + return { + updateOne: { + filter: { _id: new Types.ObjectId(secret.id) }, + update: { + $inc: { + version: 1 }, - }, - }; - } - ); + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + tags, + ...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag + ? { + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + } + : {}) + } + } + }; + }); await Secret.bulkWrite(updateOperationsToPerform); @@ -1009,7 +997,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, - tags, + tags } = secretModificationsBySecretId[secret._id.toString()]; return { @@ -1018,9 +1006,7 @@ export const updateSecrets = async (req: Request, res: Response) => { workspace: secret.workspace, type: secret.type, environment: secret.environment, - secretKeyCiphertext: secretKeyCiphertext - ? secretKeyCiphertext - : secret.secretKeyCiphertext, + secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext, secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV, secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag, secretValueCiphertext: secretValueCiphertext @@ -1031,17 +1017,13 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, - secretCommentIV: secretCommentIV - ? secretCommentIV - : secret.secretCommentIV, - secretCommentTag: secretCommentTag - ? secretCommentTag - : secret.secretCommentTag, + secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, + secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, tags: tags ? tags : secret.tags, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }; - }), + }) }; await EESecretService.addSecretVersions(secretVersions); @@ -1062,8 +1044,8 @@ export const updateSecrets = async (req: Request, res: Response) => { setTimeout(async () => { await EventService.handleEvent({ event: eventPushSecrets({ - workspaceId: new Types.ObjectId(key), - }), + workspaceId: new Types.ObjectId(key) + }) }); }, 10000); @@ -1073,7 +1055,7 @@ export const updateSecrets = async (req: Request, res: Response) => { serviceAccountId: req.serviceAccount?._id, serviceTokenDataId: req.serviceTokenData?._id, 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 @@ -1085,7 +1067,7 @@ export const updateSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key), actions: [updateAction], channel, - ipAddress: req.realIP, + ipAddress: req.realIP })); // (EE) take a secret snapshot @@ -1101,15 +1083,15 @@ export const updateSecrets = async (req: Request, res: Response) => { postHogClient.capture({ event: "secrets modified", distinctId: await TelemetryService.getDistinctId({ - authData: req.authData, + authData: req.authData }), properties: { numberOfSecrets: workspaceSecretObj[key].length, environment: workspaceSecretObj[key][0].environment, workspaceId: key, channel: channel, - userAgent: req.headers?.["user-agent"], - }, + userAgent: req.headers?.["user-agent"] + } }); } }); @@ -1117,9 +1099,9 @@ export const updateSecrets = async (req: Request, res: Response) => { return res.status(200).send({ secrets: await Secret.find({ _id: { - $in: req.secrets.map((secret: ISecret) => secret._id), - }, - }), + $in: req.secrets.map((secret: ISecret) => secret._id) + } + }) }); }; @@ -1179,12 +1161,12 @@ export const deleteSecrets = async (req: Request, res: Response) => { await Secret.deleteMany({ _id: { - $in: toDelete, - }, + $in: toDelete + } }); await EESecretService.markDeletedSecretVersions({ - secretIds: toDelete, + secretIds: toDelete }); // group secrets into workspaces so deleted secrets can @@ -1202,8 +1184,8 @@ export const deleteSecrets = async (req: Request, res: Response) => { // trigger event - push secrets await EventService.handleEvent({ event: eventPushSecrets({ - workspaceId: new Types.ObjectId(key), - }), + workspaceId: new Types.ObjectId(key) + }) }); const deleteAction = await EELogService.createAction({ name: ACTION_DELETE_SECRETS, @@ -1211,7 +1193,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { serviceAccountId: req.serviceAccount?._id, serviceTokenDataId: req.serviceTokenData?._id, 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 @@ -1223,7 +1205,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key), actions: [deleteAction], channel, - ipAddress: req.realIP, + ipAddress: req.realIP })); // (EE) take a secret snapshot @@ -1237,20 +1219,20 @@ export const deleteSecrets = async (req: Request, res: Response) => { postHogClient.capture({ event: "secrets deleted", distinctId: await TelemetryService.getDistinctId({ - authData: req.authData, + authData: req.authData }), properties: { numberOfSecrets: workspaceSecretObj[key].length, environment: workspaceSecretObj[key][0].environment, workspaceId: key, channel: channel, - userAgent: req.headers?.["user-agent"], - }, + userAgent: req.headers?.["user-agent"] + } }); } }); return res.status(200).send({ - secrets: req.secrets, + secrets: req.secrets }); }; diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 304311da7..d81fb97d6 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -2,10 +2,7 @@ import { Request, Response } from "express"; import crypto from "crypto"; import bcrypt from "bcrypt"; import { ServiceAccount, ServiceTokenData, User } from "../../models"; -import { - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, -} from "../../variables"; +import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables"; import { getSaltRounds } from "../../config"; import { BadRequestError } from "../../utils/errors"; import Folder from "../../models/folder"; @@ -46,14 +43,13 @@ export const getServiceTokenData = async (req: Request, res: Response) => { if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({ - message: "Failed accepted client validation for service token data", + message: "Failed accepted client validation for service token data" }); - const serviceTokenData = await ServiceTokenData.findById( - req.authData.authPayload._id - ) + const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id) .select("+encryptedKey +iv +tag") - .populate("user").lean(); + .populate("user") + .lean(); return res.status(200).json(serviceTokenData); }; @@ -68,29 +64,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => { let serviceTokenData; - const { - name, - workspaceId, - environment, - encryptedKey, - iv, - tag, - expiresIn, - secretPath, - permissions, - } = req.body; - - const folders = await Folder.findOne({ - workspace: workspaceId, - environment, - }); - - if (folders) { - const folder = getFolderByPath(folders.nodes, secretPath); - if (folder == undefined) { - throw BadRequestError({ message: "Path for service token does not exist" }) - } - } + const { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body; const secret = crypto.randomBytes(16).toString("hex"); const secretHash = await bcrypt.hash(secret, await getSaltRounds()); @@ -103,10 +77,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { let user, serviceAccount; - if ( - req.authData.authMode === AUTH_MODE_JWT && - req.authData.authPayload instanceof User - ) { + if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { user = req.authData.authPayload._id; } @@ -120,17 +91,16 @@ export const createServiceTokenData = async (req: Request, res: Response) => { serviceTokenData = await new ServiceTokenData({ name, workspace: workspaceId, - environment, user, serviceAccount, + scopes, lastUsed: new Date(), expiresAt, secretHash, encryptedKey, iv, tag, - secretPath, - permissions, + permissions }).save(); // return service token data without sensitive data @@ -142,7 +112,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { return res.status(200).send({ serviceToken, - serviceTokenData, + serviceTokenData }); }; @@ -155,11 +125,9 @@ export const createServiceTokenData = async (req: Request, res: Response) => { export const deleteServiceTokenData = async (req: Request, res: Response) => { const { serviceTokenDataId } = req.params; - const serviceTokenData = await ServiceTokenData.findByIdAndDelete( - serviceTokenDataId - ); + const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); return res.status(200).send({ - serviceTokenData, + serviceTokenData }); }; diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 51731bb6a..cd964fe9f 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -4,21 +4,16 @@ import { DeleteSecretParams, GetSecretParams, GetSecretsParams, - UpdateSecretParams, + UpdateSecretParams } from "../interfaces/services/SecretService"; -import { - ISecret, - Secret, - SecretBlindIndexData, - ServiceTokenData, -} from "../models"; +import { ISecret, Secret, SecretBlindIndexData, ServiceTokenData } from "../models"; import { SecretVersion } from "../ee/models"; import { BadRequestError, InternalServerError, SecretBlindIndexDataNotFoundError, SecretNotFoundError, - UnauthorizedRequestError, + UnauthorizedRequestError } from "../utils/errors"; import { ACTION_ADD_SECRETS, @@ -29,51 +24,42 @@ import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, - SECRET_SHARED, + SECRET_SHARED } from "../variables"; import crypto from "crypto"; import * as argon2 from "argon2"; import { decryptSymmetric128BitHexKeyUTF8, - encryptSymmetric128BitHexKeyUTF8, + encryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto"; import { TelemetryService } from "../services"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { EELogService, EESecretService } from "../ee/services"; -import { - getAuthDataPayloadIdObj, - getAuthDataPayloadUserObj, -} from "../utils/auth"; +import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth"; import { getFolderIdFromServiceToken } from "../services/FolderService"; +import picomatch from "picomatch"; /** * Returns an object containing secret [secret] but with its value, key, comment decrypted. - * + * * Precondition: the workspace for secret [secret] must have E2EE disabled * @param {ISecret} secret - secret to repackage to raw * @param {String} key - symmetric key to use to decrypt secret - * @returns + * @returns */ -export const repackageSecretToRaw = ({ - secret, - key, -}: { - secret: ISecret; - key: string; -}) => { - +export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: string }) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, - key, + key }); const secretValue = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretValueCiphertext, iv: secret.secretValueIV, tag: secret.secretValueTag, - key, + key }); let secretComment = ""; @@ -83,11 +69,11 @@ export const repackageSecretToRaw = ({ ciphertext: secret.secretCommentCiphertext, iv: secret.secretCommentIV, tag: secret.secretCommentTag, - key, + key }); } - return ({ + return { _id: secret._id, version: secret.version, workspace: secret.workspace, @@ -96,9 +82,9 @@ export const repackageSecretToRaw = ({ user: secret.user, secretKey, secretValue, - secretComment, - }); -} + secretComment + }; +}; /** * Create secret blind index data containing encrypted blind index [salt] @@ -107,7 +93,7 @@ export const repackageSecretToRaw = ({ * @param {Types.ObjectId} obj.workspaceId */ export const createSecretBlindIndexDataHelper = async ({ - workspaceId, + workspaceId }: { workspaceId: Types.ObjectId; }) => { @@ -121,7 +107,7 @@ export const createSecretBlindIndexDataHelper = async ({ const { ciphertext: encryptedSaltCiphertext, iv: saltIV, - tag: saltTag, + tag: saltTag } = client.encryptSymmetric(salt, rootEncryptionKey); return await new SecretBlindIndexData({ @@ -130,16 +116,16 @@ export const createSecretBlindIndexDataHelper = async ({ saltIV, saltTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64, + keyEncoding: ENCODING_SCHEME_BASE64 }).save(); } else { const { ciphertext: encryptedSaltCiphertext, iv: saltIV, - tag: saltTag, + tag: saltTag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: salt, - key: encryptionKey, + key: encryptionKey }); return await new SecretBlindIndexData({ @@ -148,7 +134,7 @@ export const createSecretBlindIndexDataHelper = async ({ saltIV, saltTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }).save(); } }; @@ -160,7 +146,7 @@ export const createSecretBlindIndexDataHelper = async ({ * @returns */ export const getSecretBlindIndexSaltHelper = async ({ - workspaceId, + workspaceId }: { workspaceId: Types.ObjectId; }) => { @@ -168,36 +154,30 @@ export const getSecretBlindIndexSaltHelper = async ({ const rootEncryptionKey = await getRootEncryptionKey(); const secretBlindIndexData = await SecretBlindIndexData.findOne({ - workspace: workspaceId, + workspace: workspaceId }).select("+algorithm +keyEncoding"); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); - if ( - rootEncryptionKey && - secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64 - ) { + if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) { return client.decryptSymmetric( secretBlindIndexData.encryptedSaltCiphertext, rootEncryptionKey, secretBlindIndexData.saltIV, secretBlindIndexData.saltTag ); - } else if ( - encryptionKey && - secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8 - ) { + } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) { // decrypt workspace salt return decryptSymmetric128BitHexKeyUTF8({ ciphertext: secretBlindIndexData.encryptedSaltCiphertext, iv: secretBlindIndexData.saltIV, tag: secretBlindIndexData.saltTag, - key: encryptionKey, + key: encryptionKey }); } throw InternalServerError({ - message: "Failed to obtain workspace salt needed for secret blind indexing", + message: "Failed to obtain workspace salt needed for secret blind indexing" }); }; @@ -210,7 +190,7 @@ export const getSecretBlindIndexSaltHelper = async ({ */ export const generateSecretBlindIndexWithSaltHelper = async ({ secretName, - salt, + salt }: { secretName: string; salt: string; @@ -224,7 +204,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({ memoryCost: 65536, // default pool of 64 MiB per thread. hashLength: 32, parallelism: 1, - raw: true, + raw: true }) ).toString("base64"); @@ -240,7 +220,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({ */ export const generateSecretBlindIndexHelper = async ({ secretName, - workspaceId, + workspaceId }: { secretName: string; workspaceId: Types.ObjectId; @@ -250,16 +230,13 @@ export const generateSecretBlindIndexHelper = async ({ const rootEncryptionKey = await getRootEncryptionKey(); const secretBlindIndexData = await SecretBlindIndexData.findOne({ - workspace: workspaceId, + workspace: workspaceId }).select("+algorithm +keyEncoding"); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); let salt; - if ( - rootEncryptionKey && - secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64 - ) { + if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) { salt = client.decryptSymmetric( secretBlindIndexData.encryptedSaltCiphertext, rootEncryptionKey, @@ -269,32 +246,29 @@ export const generateSecretBlindIndexHelper = async ({ const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ secretName, - salt, + salt }); return secretBlindIndex; - } else if ( - encryptionKey && - secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8 - ) { + } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) { // decrypt workspace salt salt = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secretBlindIndexData.encryptedSaltCiphertext, iv: secretBlindIndexData.saltIV, tag: secretBlindIndexData.saltTag, - key: encryptionKey, + key: encryptionKey }); const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ secretName, - salt, + salt }); return secretBlindIndex; } throw InternalServerError({ - message: "Failed to generate secret blind index", + message: "Failed to generate secret blind index" }); }; @@ -323,38 +297,39 @@ export const createSecretHelper = async ({ secretCommentCiphertext, secretCommentIV, secretCommentTag, - secretPath = "/", + secretPath = "/" }: CreateSecretParams) => { - const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); // if using service token filter towards the folderId by secretpath if (authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; - if (secretPath !== serviceTkScopedSecretPath) { + const { scopes: tkScopes } = authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); + + if (!validScope) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } - const folderId = await getFolderIdFromServiceToken( - workspaceId, - environment, - secretPath - ); + const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); const exists = await Secret.exists({ secretBlindIndex, workspace: new Types.ObjectId(workspaceId), folder: folderId, type, - ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}) }); if (exists) throw BadRequestError({ - message: "Failed to create secret that already exists", + message: "Failed to create secret that already exists" }); if (type === SECRET_PERSONAL) { @@ -365,13 +340,12 @@ export const createSecretHelper = async ({ secretBlindIndex, folder: folderId, workspace: new Types.ObjectId(workspaceId), - type: SECRET_SHARED, + type: SECRET_SHARED }); if (!exists) throw BadRequestError({ - message: - "Failed to create personal secret override for no corresponding shared secret", + message: "Failed to create personal secret override for no corresponding shared secret" }); } @@ -394,7 +368,7 @@ export const createSecretHelper = async ({ secretCommentTag, folder: folderId, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }).save(); const secretVersion = new SecretVersion({ @@ -414,12 +388,12 @@ export const createSecretHelper = async ({ secretValueIV, secretValueTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }); // (EE) add version for new secret await EESecretService.addSecretVersions({ - secretVersions: [secretVersion], + secretVersions: [secretVersion] }); // (EE) create (audit) log @@ -427,7 +401,7 @@ export const createSecretHelper = async ({ name: ACTION_ADD_SECRETS, ...getAuthDataPayloadIdObj(authData), workspaceId, - secretIds: [secret._id], + secretIds: [secret._id] }); action && @@ -436,14 +410,14 @@ export const createSecretHelper = async ({ workspaceId, actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP, + ipAddress: authData.authIP })); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId, environment, - folderId, + folderId }); const postHogClient = await TelemetryService.getPostHogClient(); @@ -452,7 +426,7 @@ export const createSecretHelper = async ({ postHogClient.capture({ event: "secrets added", distinctId: await TelemetryService.getDistinctId({ - authData, + authData }), properties: { numberOfSecrets: 1, @@ -460,8 +434,8 @@ export const createSecretHelper = async ({ workspaceId, folderId, channel: authData.authChannel, - userAgent: authData.authUserAgent, - }, + userAgent: authData.authUserAgent + } }); } @@ -480,21 +454,23 @@ export const getSecretsHelper = async ({ workspaceId, environment, authData, - secretPath = "/", + secretPath = "/" }: GetSecretsParams) => { let secrets: ISecret[] = []; // if using service token filter towards the folderId by secretpath if (authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; - if (secretPath !== serviceTkScopedSecretPath) { + const { scopes: tkScopes } = authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); + + if (!validScope) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } - const folderId = await getFolderIdFromServiceToken( - workspaceId, - environment, - secretPath - ); + const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); // get personal secrets first secrets = await Secret.find({ @@ -502,8 +478,10 @@ export const getSecretsHelper = async ({ environment, folder: folderId, type: SECRET_PERSONAL, - ...getAuthDataPayloadUserObj(authData), - }).populate("tags").lean(); + ...getAuthDataPayloadUserObj(authData) + }) + .populate("tags") + .lean(); // concat with shared secrets secrets = secrets.concat( @@ -513,9 +491,11 @@ export const getSecretsHelper = async ({ folder: folderId, type: SECRET_SHARED, secretBlindIndex: { - $nin: secrets.map((secret) => secret.secretBlindIndex), - }, - }).populate("tags").lean() + $nin: secrets.map((secret) => secret.secretBlindIndex) + } + }) + .populate("tags") + .lean() ); // (EE) create (audit) log @@ -523,7 +503,7 @@ export const getSecretsHelper = async ({ name: ACTION_READ_SECRETS, ...getAuthDataPayloadIdObj(authData), workspaceId, - secretIds: secrets.map((secret) => secret._id), + secretIds: secrets.map((secret) => secret._id) }); action && @@ -532,7 +512,7 @@ export const getSecretsHelper = async ({ workspaceId, actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP, + ipAddress: authData.authIP })); const postHogClient = await TelemetryService.getPostHogClient(); @@ -541,7 +521,7 @@ export const getSecretsHelper = async ({ postHogClient.capture({ event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ - authData, + authData }), properties: { numberOfSecrets: secrets.length, @@ -549,8 +529,8 @@ export const getSecretsHelper = async ({ workspaceId, folderId, channel: authData.authChannel, - userAgent: authData.authUserAgent, - }, + userAgent: authData.authUserAgent + } }); } @@ -573,25 +553,27 @@ export const getSecretHelper = async ({ environment, type, authData, - secretPath = "/", + secretPath = "/" }: GetSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); let secret: ISecret | null = null; // if using service token filter towards the folderId by secretpath if (authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; - if (secretPath !== serviceTkScopedSecretPath) { + const { scopes: tkScopes } = authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); + + if (!validScope) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } - const folderId = await getFolderIdFromServiceToken( - workspaceId, - environment, - secretPath - ); + const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); // try getting personal secret first (if exists) secret = await Secret.findOne({ @@ -600,7 +582,7 @@ export const getSecretHelper = async ({ environment, folder: folderId, type: type ?? SECRET_PERSONAL, - ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}) }).lean(); if (!secret) { @@ -611,7 +593,7 @@ export const getSecretHelper = async ({ workspace: new Types.ObjectId(workspaceId), environment, folder: folderId, - type: SECRET_SHARED, + type: SECRET_SHARED }).lean(); } @@ -622,7 +604,7 @@ export const getSecretHelper = async ({ name: ACTION_READ_SECRETS, ...getAuthDataPayloadIdObj(authData), workspaceId, - secretIds: [secret._id], + secretIds: [secret._id] }); action && @@ -631,7 +613,7 @@ export const getSecretHelper = async ({ workspaceId, actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP, + ipAddress: authData.authIP })); const postHogClient = await TelemetryService.getPostHogClient(); @@ -640,7 +622,7 @@ export const getSecretHelper = async ({ postHogClient.capture({ event: "secrets pull", distinctId: await TelemetryService.getDistinctId({ - authData, + authData }), properties: { numberOfSecrets: 1, @@ -648,8 +630,8 @@ export const getSecretHelper = async ({ workspaceId, folderId, channel: authData.authChannel, - userAgent: authData.authUserAgent, - }, + userAgent: authData.authUserAgent + } }); } @@ -679,26 +661,28 @@ export const updateSecretHelper = async ({ secretValueCiphertext, secretValueIV, secretValueTag, - secretPath, + secretPath }: UpdateSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); let secret: ISecret | null = null; // if using service token filter towards the folderId by secretpath if (authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; - if (secretPath !== serviceTkScopedSecretPath) { + const { scopes: tkScopes } = authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); + + if (!validScope) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } - const folderId = await getFolderIdFromServiceToken( - workspaceId, - environment, - secretPath - ); + const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); if (type === SECRET_SHARED) { // case: update shared secret @@ -708,16 +692,16 @@ export const updateSecretHelper = async ({ workspace: new Types.ObjectId(workspaceId), environment, folder: folderId, - type, + type }, { secretValueCiphertext, secretValueIV, secretValueTag, - $inc: { version: 1 }, + $inc: { version: 1 } }, { - new: true, + new: true } ); } else { @@ -730,16 +714,16 @@ export const updateSecretHelper = async ({ environment, type, folder: folderId, - ...getAuthDataPayloadUserObj(authData), + ...getAuthDataPayloadUserObj(authData) }, { secretValueCiphertext, secretValueIV, secretValueTag, - $inc: { version: 1 }, + $inc: { version: 1 } }, { - new: true, + new: true } ); } @@ -763,12 +747,12 @@ export const updateSecretHelper = async ({ secretValueIV, secretValueTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }); // (EE) add version for new secret await EESecretService.addSecretVersions({ - secretVersions: [secretVersion], + secretVersions: [secretVersion] }); // (EE) create (audit) log @@ -776,7 +760,7 @@ export const updateSecretHelper = async ({ name: ACTION_UPDATE_SECRETS, ...getAuthDataPayloadIdObj(authData), workspaceId, - secretIds: [secret._id], + secretIds: [secret._id] }); action && @@ -785,14 +769,14 @@ export const updateSecretHelper = async ({ workspaceId, actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP, + ipAddress: authData.authIP })); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId, environment, - folderId: secret?.folder, + folderId: secret?.folder }); const postHogClient = await TelemetryService.getPostHogClient(); @@ -801,7 +785,7 @@ export const updateSecretHelper = async ({ postHogClient.capture({ event: "secrets modified", distinctId: await TelemetryService.getDistinctId({ - authData, + authData }), properties: { numberOfSecrets: 1, @@ -809,8 +793,8 @@ export const updateSecretHelper = async ({ workspaceId, folderId, channel: authData.authChannel, - userAgent: authData.authUserAgent, - }, + userAgent: authData.authUserAgent + } }); } @@ -833,26 +817,27 @@ export const deleteSecretHelper = async ({ environment, type, authData, - secretPath = "/", + secretPath = "/" }: DeleteSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); // if using service token filter towards the folderId by secretpath if (authData.authPayload instanceof ServiceTokenData) { - const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; + const { scopes: tkScopes } = authData.authPayload; + const validScope = tkScopes.find( + (scope) => + picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) && + scope.environment === environment + ); - if (secretPath !== serviceTkScopedSecretPath) { + if (!validScope) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } } - const folderId = await getFolderIdFromServiceToken( - workspaceId, - environment, - secretPath - ); + const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); let secrets: ISecret[] = []; let secret: ISecret | null = null; @@ -862,7 +847,7 @@ export const deleteSecretHelper = async ({ secretBlindIndex, workspaceId: new Types.ObjectId(workspaceId), environment, - folder: folderId, + folder: folderId }).lean(); secret = await Secret.findOneAndDelete({ @@ -870,14 +855,14 @@ export const deleteSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId), environment, type, - folder: folderId, + folder: folderId }).lean(); await Secret.deleteMany({ secretBlindIndex, workspaceId: new Types.ObjectId(workspaceId), environment, - folder: folderId, + folder: folderId }); } else { secret = await Secret.findOneAndDelete({ @@ -886,7 +871,7 @@ export const deleteSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId), environment, type, - ...getAuthDataPayloadUserObj(authData), + ...getAuthDataPayloadUserObj(authData) }).lean(); if (secret) { @@ -897,7 +882,7 @@ export const deleteSecretHelper = async ({ if (!secret) throw SecretNotFoundError(); await EESecretService.markDeletedSecretVersions({ - secretIds: secrets.map((secret) => secret._id), + secretIds: secrets.map((secret) => secret._id) }); // (EE) create (audit) log @@ -905,22 +890,23 @@ export const deleteSecretHelper = async ({ name: ACTION_DELETE_SECRETS, ...getAuthDataPayloadIdObj(authData), workspaceId, - secretIds: secrets.map((secret) => secret._id), + secretIds: secrets.map((secret) => secret._id) }); - action && (await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP, - })); + action && + (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP + })); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId, environment, - folderId: secret?.folder, + folderId: secret?.folder }); const postHogClient = await TelemetryService.getPostHogClient(); @@ -929,7 +915,7 @@ export const deleteSecretHelper = async ({ postHogClient.capture({ event: "secrets deleted", distinctId: await TelemetryService.getDistinctId({ - authData, + authData }), properties: { numberOfSecrets: secrets.length, @@ -937,13 +923,13 @@ export const deleteSecretHelper = async ({ workspaceId, folderId, channel: authData.authChannel, - userAgent: authData.authUserAgent, - }, + userAgent: authData.authUserAgent + } }); } - return ({ + return { secrets, - secret, - }); + secret + }; }; diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index 57528a4e9..804184386 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -4,7 +4,10 @@ export interface IServiceTokenData extends Document { _id: Types.ObjectId; name: string; workspace: Types.ObjectId; - environment: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; user: Types.ObjectId; serviceAccount: Types.ObjectId; lastUsed: Date; @@ -13,7 +16,6 @@ export interface IServiceTokenData extends Document { encryptedKey: string; iv: string; tag: string; - secretPath: string; permissions: string[]; } @@ -21,68 +23,72 @@ const serviceTokenDataSchema = new Schema( { name: { type: String, - required: true, + required: true }, workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true, + required: true }, - environment: { - type: String, - required: true, + scopes: { + type: [ + { + environment: { + type: String, + required: true + }, + secretPath: { + type: String, + default: "/", + required: true + } + } + ], + required: true }, user: { type: Schema.Types.ObjectId, ref: "User", - required: true, + required: true }, serviceAccount: { type: Schema.Types.ObjectId, - ref: "ServiceAccount", + ref: "ServiceAccount" }, lastUsed: { - type: Date, + type: Date }, expiresAt: { - type: Date, + type: Date }, secretHash: { type: String, required: true, - select: false, + select: false }, encryptedKey: { type: String, - select: false, + select: false }, iv: { type: String, - select: false, + select: false }, tag: { type: String, - select: false, + select: false }, permissions: { type: [String], enum: ["read", "write"], - default: ["read"], - }, - secretPath: { - type: String, - default: "/", - required: true, - }, + default: ["read"] + } }, { - timestamps: true, + timestamps: true } ); -const ServiceTokenData = model( - "ServiceTokenData", - serviceTokenDataSchema -); +const ServiceTokenData = model("ServiceTokenData", serviceTokenDataSchema); export default ServiceTokenData; diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 33ffad3cf..84f443deb 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -4,7 +4,7 @@ import { requireAuth, requireServiceTokenDataAuth, requireWorkspaceAuth, - validateRequest, + validateRequest } from "../../middleware"; import { body, param } from "express-validator"; import { @@ -13,14 +13,14 @@ import { AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, MEMBER, - PERMISSION_WRITE_SECRETS, + PERMISSION_WRITE_SECRETS } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; router.get( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN], + acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN] }), serviceTokenDataController.getServiceTokenData ); @@ -28,33 +28,30 @@ router.get( router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT], + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), body("name").exists().isString().trim(), body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), + body("scopes").exists().isArray(), + body("scopes.*.environment").exists().isString().trim(), + body("scopes.*.secretPath").exists().isString().trim(), body("encryptedKey").exists().isString().trim(), body("iv").exists().isString().trim(), - body("secretPath").isString().default("/").trim(), body("tag").exists().isString().trim(), body("expiresIn").exists().isNumeric(), // measured in ms body("permissions") .isArray({ min: 1 }) .custom((value: string[]) => { const allowedPermissions = ["read", "write"]; - const invalidValues = value.filter( - (v) => !allowedPermissions.includes(v) - ); + const invalidValues = value.filter((v) => !allowedPermissions.includes(v)); if (invalidValues.length > 0) { - throw new Error( - `permissions contains invalid values: ${invalidValues.join(", ")}` - ); + throw new Error(`permissions contains invalid values: ${invalidValues.join(", ")}`); } return true; @@ -66,10 +63,10 @@ router.post( router.delete( "/:serviceTokenDataId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AUTH_MODE_JWT] }), requireServiceTokenDataAuth({ - acceptedRoles: [ADMIN, MEMBER], + acceptedRoles: [ADMIN, MEMBER] }), param("serviceTokenDataId").exists().trim(), validateRequest, diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index a77d3ee27..98dcad90e 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -13,14 +13,14 @@ import { Secret, SecretBlindIndexData, ServiceTokenData, - Workspace, + Workspace } from "../../models"; import { generateKeyPair } from "../../utils/crypto"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_UTF8 } from "../../variables"; import { InternalServerError } from "../errors"; @@ -29,10 +29,7 @@ import { InternalServerError } from "../errors"; * corresponding secret versions */ export const backfillSecretVersions = async () => { - await Secret.updateMany( - { version: { $exists: false } }, - { $set: { version: 1 } } - ); + await Secret.updateMany({ version: { $exists: false } }, { $set: { version: 1 } }); const unversionedSecrets: ISecret[] = await Secret.aggregate([ { @@ -40,14 +37,14 @@ export const backfillSecretVersions = async () => { from: "secretversions", localField: "_id", foreignField: "secret", - as: "versions", - }, + as: "versions" + } }, { $match: { - versions: { $size: 0 }, - }, - }, + versions: { $size: 0 } + } + } ]); if (unversionedSecrets.length > 0) { @@ -62,9 +59,9 @@ export const backfillSecretVersions = async () => { workspace: s.workspace, environment: s.environment, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }) - ), + ) }); } console.log("Migration: Secret version migration v1 complete"); @@ -80,8 +77,8 @@ export const backfillBots = async () => { const workspaceIdsWithBot = await Bot.distinct("workspace"); const workspaceIdsToAddBot = await Workspace.distinct("_id", { _id: { - $nin: workspaceIdsWithBot, - }, + $nin: workspaceIdsWithBot + } }); if (workspaceIdsToAddBot.length === 0) return; @@ -94,7 +91,7 @@ export const backfillBots = async () => { const { ciphertext: encryptedPrivateKey, iv, - tag, + tag } = client.encryptSymmetric(privateKey, rootEncryptionKey); return new Bot({ @@ -106,16 +103,16 @@ export const backfillBots = async () => { iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64, + keyEncoding: ENCODING_SCHEME_BASE64 }); } else if (encryptionKey) { const { ciphertext: encryptedPrivateKey, iv, - tag, + tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: privateKey, - key: encryptionKey, + key: encryptionKey }); return new Bot({ @@ -127,13 +124,12 @@ export const backfillBots = async () => { iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }); } throw InternalServerError({ - message: - "Failed to backfill workspace bots due to missing encryption key", + message: "Failed to backfill workspace bots due to missing encryption key" }); }) ); @@ -149,13 +145,11 @@ export const backfillSecretBlindIndexData = async () => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); - const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct( - "workspace" - ); + const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct("workspace"); const workspaceIdsToBlindIndex = await Workspace.distinct("_id", { _id: { - $nin: workspaceIdsBlindIndexed, - }, + $nin: workspaceIdsBlindIndexed + } }); if (workspaceIdsToBlindIndex.length === 0) return; @@ -168,7 +162,7 @@ export const backfillSecretBlindIndexData = async () => { const { ciphertext: encryptedSaltCiphertext, iv: saltIV, - tag: saltTag, + tag: saltTag } = client.encryptSymmetric(salt, rootEncryptionKey); return new SecretBlindIndexData({ @@ -177,16 +171,16 @@ export const backfillSecretBlindIndexData = async () => { saltIV, saltTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64, + keyEncoding: ENCODING_SCHEME_BASE64 }); } else if (encryptionKey) { const { ciphertext: encryptedSaltCiphertext, iv: saltIV, - tag: saltTag, + tag: saltTag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: salt, - key: encryptionKey, + key: encryptionKey }); return new SecretBlindIndexData({ @@ -195,13 +189,12 @@ export const backfillSecretBlindIndexData = async () => { saltIV, saltTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, + keyEncoding: ENCODING_SCHEME_UTF8 }); } throw InternalServerError({ - message: - "Failed to backfill secret blind index data due to missing encryption key", + message: "Failed to backfill secret blind index data due to missing encryption key" }); }) ); @@ -219,17 +212,17 @@ export const backfillEncryptionMetadata = async () => { await Secret.updateMany( { algorithm: { - $exists: false, + $exists: false }, keyEncoding: { - $exists: false, - }, + $exists: false + } }, { $set: { algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - }, + keyEncoding: ENCODING_SCHEME_UTF8 + } } ); @@ -237,17 +230,17 @@ export const backfillEncryptionMetadata = async () => { await SecretVersion.updateMany( { algorithm: { - $exists: false, + $exists: false }, keyEncoding: { - $exists: false, - }, + $exists: false + } }, { $set: { algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - }, + keyEncoding: ENCODING_SCHEME_UTF8 + } } ); @@ -255,17 +248,17 @@ export const backfillEncryptionMetadata = async () => { await SecretBlindIndexData.updateMany( { algorithm: { - $exists: false, + $exists: false }, keyEncoding: { - $exists: false, - }, + $exists: false + } }, { $set: { algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - }, + keyEncoding: ENCODING_SCHEME_UTF8 + } } ); @@ -273,17 +266,17 @@ export const backfillEncryptionMetadata = async () => { await Bot.updateMany( { algorithm: { - $exists: false, + $exists: false }, keyEncoding: { - $exists: false, - }, + $exists: false + } }, { $set: { algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - }, + keyEncoding: ENCODING_SCHEME_UTF8 + } } ); @@ -291,17 +284,17 @@ export const backfillEncryptionMetadata = async () => { await BackupPrivateKey.updateMany( { algorithm: { - $exists: false, + $exists: false }, keyEncoding: { - $exists: false, - }, + $exists: false + } }, { $set: { algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - }, + keyEncoding: ENCODING_SCHEME_UTF8 + } } ); @@ -309,17 +302,17 @@ export const backfillEncryptionMetadata = async () => { await IntegrationAuth.updateMany( { algorithm: { - $exists: false, + $exists: false }, keyEncoding: { - $exists: false, - }, + $exists: false + } }, { $set: { algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - }, + keyEncoding: ENCODING_SCHEME_UTF8 + } } ); }; @@ -328,26 +321,26 @@ export const backfillSecretFolders = async () => { await Secret.updateMany( { folder: { - $exists: false, - }, + $exists: false + } }, { $set: { - folder: "root", - }, + folder: "root" + } } ); await SecretVersion.updateMany( { folder: { - $exists: false, - }, + $exists: false + } }, { $set: { - folder: "root", - }, + folder: "root" + } } ); @@ -355,20 +348,20 @@ export const backfillSecretFolders = async () => { await SecretVersion.updateMany( { tags: { - $exists: false, - }, + $exists: false + } }, { $set: { - tags: [], - }, + tags: [] + } } ); let secretSnapshots = await SecretSnapshot.find({ environment: { - $exists: false, - }, + $exists: false + } }) .populate<{ secretVersions: ISecretVersion[] }>("secretVersions") .limit(50); @@ -377,8 +370,7 @@ export const backfillSecretFolders = async () => { for (const secSnapshot of secretSnapshots) { const groupSnapByEnv: Record> = {}; secSnapshot.secretVersions.forEach((secVer) => { - if (!groupSnapByEnv?.[secVer.environment]) - groupSnapByEnv[secVer.environment] = []; + if (!groupSnapByEnv?.[secVer.environment]) groupSnapByEnv[secVer.environment] = []; groupSnapByEnv[secVer.environment].push(secVer); }); @@ -390,7 +382,7 @@ export const backfillSecretFolders = async () => { ...secSnapshot.toObject({ virtuals: false }), _id: new Types.ObjectId(), environment: snapEnv, - secretVersions: secretIdsOfEnvGroup, + secretVersions: secretIdsOfEnvGroup }; }); @@ -400,8 +392,8 @@ export const backfillSecretFolders = async () => { secretSnapshots = await SecretSnapshot.find({ environment: { - $exists: false, - }, + $exists: false + } }) .populate<{ secretVersions: ISecretVersion[] }>("secretVersions") .limit(50); @@ -414,13 +406,13 @@ export const backfillServiceToken = async () => { await ServiceTokenData.updateMany( { secretPath: { - $exists: false, - }, + $exists: false + } }, { $set: { - secretPath: "/", - }, + secretPath: "/" + } } ); console.log("Migration: Service token migration v1 complete"); @@ -430,14 +422,36 @@ export const backfillIntegration = async () => { await Integration.updateMany( { secretPath: { - $exists: false, - }, + $exists: false + } }, { $set: { - secretPath: "/", - }, + secretPath: "/" + } } ); console.log("Migration: Integration migration v1 complete"); }; + +export const backfillServiceTokenMultiScope = async () => { + await ServiceTokenData.updateMany( + { + scopes: { + $exists: false + } + }, + [ + { + $set: { + scopes: [{ environment: "$environment", secretPath: "$secretPath" }] + } + }, + { + $unset: ["environment", "secretPath"] + } + ] + ); + + console.log("Migration: Service token migration v2 complete"); +}; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 16d9ee1ae..00f41fe67 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -14,17 +14,15 @@ import { backfillSecretFolders, backfillSecretVersions, backfillServiceToken, + backfillServiceTokenMultiScope } from "./backfillData"; -import { - reencryptBotPrivateKeys, - reencryptSecretBlindIndexDataSalts, -} from "./reencryptData"; +import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData"; import { getClientIdGoogle, getClientSecretGoogle, getMongoURL, getNodeEnv, - getSentryDSN, + getSentryDSN } from "../../config"; import { initializePassport } from "../auth"; @@ -79,6 +77,7 @@ export const setup = async () => { await backfillSecretFolders(); await backfillServiceToken(); await backfillIntegration(); + await backfillServiceTokenMultiScope(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY @@ -90,7 +89,7 @@ export const setup = async () => { dsn: await getSentryDSN(), tracesSampleRate: 1.0, debug: (await getNodeEnv()) === "production" ? false : true, - environment: await getNodeEnv(), + environment: await getNodeEnv() }); await createTestUserForDevelopment(); diff --git a/backend/src/validation/serviceTokenData.ts b/backend/src/validation/serviceTokenData.ts index 0ade7f96e..580bcbdb9 100644 --- a/backend/src/validation/serviceTokenData.ts +++ b/backend/src/validation/serviceTokenData.ts @@ -1,22 +1,19 @@ import { Types } from "mongoose"; import { - ISecret, - IServiceAccount, - IServiceTokenData, - IUser, - ServiceAccount, - ServiceTokenData, - User, + ISecret, + IServiceAccount, + IServiceTokenData, + IUser, + ServiceAccount, + ServiceTokenData, + User } from "../models"; -import { - ServiceTokenDataNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { ServiceTokenDataNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN } from "../variables"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; @@ -30,65 +27,71 @@ import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles */ export const validateClientForServiceTokenData = async ({ - authData, - serviceTokenDataId, - acceptedRoles, + authData, + serviceTokenDataId, + acceptedRoles }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - serviceTokenDataId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + serviceTokenDataId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; }) => { - const serviceTokenData = await ServiceTokenData - .findById(serviceTokenDataId) - .select("+encryptedKey +iv +tag") - .populate<{ user: IUser }>("user"); + const serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId) + .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" }); - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: serviceTokenData.workspace, - acceptedRoles, - }); - - return serviceTokenData; - } + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: serviceTokenData.workspace, + acceptedRoles + }); - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: serviceTokenData.workspace, - }); - - return serviceTokenData; - } + return serviceTokenData; + } - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for service token data", - }); - } + if ( + authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && + authData.authPayload instanceof ServiceAccount + ) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: serviceTokenData.workspace + }); - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: serviceTokenData.workspace, - acceptedRoles, - }); - - return serviceTokenData; - } - + return serviceTokenData; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_TOKEN && + authData.authPayload instanceof ServiceTokenData + ) { throw UnauthorizedRequestError({ - message: "Failed client authorization for service token data", + message: "Failed service token authorization for service token data" }); -} + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: serviceTokenData.workspace, + acceptedRoles + }); + + return serviceTokenData; + } + + throw UnauthorizedRequestError({ + message: "Failed client authorization for service token data" + }); +}; /** * Validate that service token (client) can access workspace @@ -101,42 +104,42 @@ export const validateClientForServiceTokenData = async ({ * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateServiceTokenDataClientForWorkspace = async ({ - serviceTokenData, - workspaceId, - environment, - requiredPermissions, + serviceTokenData, + workspaceId, + environment, + requiredPermissions }: { - serviceTokenData: IServiceTokenData; - workspaceId: Types.ObjectId; - environment?: string; - requiredPermissions?: string[]; + serviceTokenData: IServiceTokenData; + workspaceId: Types.ObjectId; + environment?: string; + requiredPermissions?: string[]; }) => { - if (!serviceTokenData.workspace.equals(workspaceId)) { - // case: invalid workspaceId passed + if (!serviceTokenData.workspace.equals(workspaceId)) { + // case: invalid workspaceId passed + throw UnauthorizedRequestError({ + message: "Failed service token authorization for the given workspace" + }); + } + + if (environment) { + // case: environment is specified + + if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === 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", + message: `Failed service token authorization for the given workspace environment action: ${permission}` }); - } - - if (environment) { - // case: environment is specified - - 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 @@ -147,36 +150,35 @@ export const validateServiceTokenDataClientForWorkspace = async ({ * @param {string[]} requiredPermissions - required permissions as part of the endpoint */ export const validateServiceTokenDataClientForSecrets = async ({ - serviceTokenData, - secrets, - requiredPermissions, + serviceTokenData, + secrets, + requiredPermissions }: { - serviceTokenData: IServiceTokenData; - secrets: ISecret[]; - requiredPermissions?: string[]; + 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" + }); + } - 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}`, - }); - } + if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === 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}` }); + } }); -} \ No newline at end of file + }); +};