Merge pull request #713 from akhilmhdh/feat/secret-reference

secret reference
This commit is contained in:
Maidul Islam
2023-07-07 19:02:57 -04:00
committed by GitHub
34 changed files with 1627 additions and 1162 deletions
+223 -32
View File
@@ -71,6 +71,7 @@
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12", "@types/passport": "^1.0.12",
"@types/picomatch": "^2.3.0",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1", "@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3", "@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": { "node_modules/@jest/schemas": {
"version": "29.4.3", "version": "29.4.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
@@ -3241,6 +3262,12 @@
"@types/express": "*" "@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": { "node_modules/@types/prettier": {
"version": "2.7.2", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
@@ -5680,25 +5707,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "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": { "node_modules/jest-diff": {
"version": "29.5.0", "version": "29.5.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", "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": "^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": { "node_modules/jest-snapshot": {
"version": "29.5.0", "version": "29.5.0",
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz",
@@ -11071,6 +11119,25 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/ripemd160": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@@ -11669,6 +11736,25 @@
"node": ">=0.4.0" "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": { "node_modules/swagger-ui-dist": {
"version": "4.19.0", "version": "4.19.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.0.tgz",
@@ -11723,6 +11809,26 @@
"node": ">=8" "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": { "node_modules/text-hex": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -14154,6 +14260,22 @@
"string-length": "^4.0.1", "string-length": "^4.0.1",
"strip-ansi": "^6.0.0", "strip-ansi": "^6.0.0",
"v8-to-istanbul": "^9.0.1" "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": { "@jest/schemas": {
@@ -14988,6 +15110,12 @@
"@types/express": "*" "@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": { "@types/prettier": {
"version": "2.7.2", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
@@ -16808,19 +16936,6 @@
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"dev": true "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": { "glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -17371,6 +17486,22 @@
"pretty-format": "^29.5.0", "pretty-format": "^29.5.0",
"slash": "^3.0.0", "slash": "^3.0.0",
"strip-json-comments": "^3.1.1" "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": { "jest-diff": {
@@ -17606,6 +17737,22 @@
"jest-util": "^29.5.0", "jest-util": "^29.5.0",
"slash": "^3.0.0", "slash": "^3.0.0",
"strip-bom": "^4.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": { "jest-snapshot": {
@@ -20674,6 +20821,21 @@
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": { "requires": {
"glob": "^7.1.3" "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": { "ripemd160": {
@@ -21129,6 +21291,19 @@
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" "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", "@istanbuljs/schema": "^0.1.2",
"glob": "^7.1.4", "glob": "^7.1.4",
"minimatch": "^3.0.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": { "text-hex": {
+1
View File
@@ -89,6 +89,7 @@
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12", "@types/passport": "^1.0.12",
"@types/picomatch": "^2.3.0",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1", "@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
+170 -196
View File
@@ -9,7 +9,7 @@ import {
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL
} from "../../variables"; } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EventService } from "../../services"; import { EventService } from "../../services";
@@ -21,7 +21,7 @@ import { PERMISSION_WRITE_SECRETS } from "../../variables";
import { import {
userHasNoAbility, userHasNoAbility,
userHasWorkspaceAccess, userHasWorkspaceAccess,
userHasWriteOnlyAbility, userHasWriteOnlyAbility
} from "../../ee/helpers/checkMembershipPermissions"; } from "../../ee/helpers/checkMembershipPermissions";
import Tag from "../../models/tag"; import Tag from "../../models/tag";
import _ from "lodash"; import _ from "lodash";
@@ -30,8 +30,9 @@ import Folder from "../../models/folder";
import { import {
getFolderByPath, getFolderByPath,
getFolderIdFromServiceToken, getFolderIdFromServiceToken,
searchByFolderId, searchByFolderId
} from "../../services/FolderService"; } from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets";
/** /**
* Peform a batch of any specified CUD secret operations * Peform a batch of any specified CUD secret operations
@@ -47,7 +48,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
environment, environment,
requests, requests,
secretPath, secretPath
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
@@ -63,7 +64,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
@@ -73,22 +74,17 @@ export const batchSecrets = async (req: Request, res: Response) => {
} }
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
// in service token when not giving secretpath folderid must be root // in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used // this is to avoid giving folderid when service tokens are used
if ( if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
if (secretPath) { if (secretPath) {
folderId = await getFolderIdFromServiceToken( folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
} }
for await (const request of requests) { for await (const request of requests) {
@@ -97,12 +93,10 @@ export const batchSecrets = async (req: Request, res: Response) => {
let secretBlindIndex = ""; let secretBlindIndex = "";
switch (request.method) { switch (request.method) {
case "POST": case "POST":
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
{ secretName: request.secret.secretName,
secretName: request.secret.secretName, salt
salt, });
}
);
createSecrets.push({ createSecrets.push({
...request.secret, ...request.secret,
@@ -113,16 +107,14 @@ export const batchSecrets = async (req: Request, res: Response) => {
folder: folderId, folder: folderId,
secretBlindIndex, secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
break; break;
case "PATCH": case "PATCH":
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
{ secretName: request.secret.secretName,
secretName: request.secret.secretName, salt
salt, });
}
);
updateSecrets.push({ updateSecrets.push({
...request.secret, ...request.secret,
@@ -130,7 +122,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
secretBlindIndex, secretBlindIndex,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
break; break;
case "DELETE": case "DELETE":
@@ -150,9 +142,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
...n._doc, ...n._doc,
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
secret: n._id, secret: n._id,
isDeleted: false, isDeleted: false
}; };
}), })
}); });
const addAction = (await EELogService.createAction({ const addAction = (await EELogService.createAction({
@@ -161,7 +153,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id), secretIds: createdSecrets.map((n) => n._id)
})) as IAction; })) as IAction;
actions.push(addAction); actions.push(addAction);
@@ -175,8 +167,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
folderId, folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
} }
@@ -195,7 +187,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
listedSecretsObj = req.secrets.reduce( listedSecretsObj = req.secrets.reduce(
(obj: any, secret: ISecret) => ({ (obj: any, secret: ISecret) => ({
...obj, ...obj,
[secret._id.toString()]: secret, [secret._id.toString()]: secret
}), }),
{} {}
); );
@@ -204,16 +196,16 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateOne: { updateOne: {
filter: { filter: {
_id: new Types.ObjectId(u._id), _id: new Types.ObjectId(u._id),
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId)
}, },
update: { update: {
$inc: { $inc: {
version: 1, version: 1
}, },
...u, ...u,
_id: new Types.ObjectId(u._id), _id: new Types.ObjectId(u._id)
}, }
}, }
})); }));
await Secret.bulkWrite(updateOperations); await Secret.bulkWrite(updateOperations);
@@ -240,25 +232,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags, tags: u.tags,
folder: u.folder, folder: u.folder
}) })
); );
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions, secretVersions
}); });
updatedSecrets = await Secret.find({ updatedSecrets = await Secret.find({
_id: { _id: {
$in: updateSecrets.map((u) => new Types.ObjectId(u._id)), $in: updateSecrets.map((u) => new Types.ObjectId(u._id))
}, }
}); });
const updateAction = (await EELogService.createAction({ const updateAction = (await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
userId: req.user._id, userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: updatedSecrets.map((u) => u._id), secretIds: updatedSecrets.map((u) => u._id)
})) as IAction; })) as IAction;
actions.push(updateAction); actions.push(updateAction);
@@ -272,8 +264,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
folderId, folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
} }
@@ -282,19 +274,19 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (deleteSecrets.length > 0) { if (deleteSecrets.length > 0) {
await Secret.deleteMany({ await Secret.deleteMany({
_id: { _id: {
$in: deleteSecrets, $in: deleteSecrets
}, }
}); });
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: deleteSecrets, secretIds: deleteSecrets
}); });
const deleteAction = (await EELogService.createAction({ const deleteAction = (await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId: req.user._id, userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: deleteSecrets, secretIds: deleteSecrets
})) as IAction; })) as IAction;
actions.push(deleteAction); actions.push(deleteAction);
@@ -307,8 +299,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel: channel, channel: channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
} }
@@ -320,22 +312,22 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions, actions,
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
}); });
} }
// // trigger event - push secrets // // trigger event - push secrets
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}), })
}); });
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId, folderId
}); });
const resObj: { [key: string]: ISecret[] | string[] } = {}; const resObj: { [key: string]: ISecret[] | string[] } = {};
@@ -418,7 +410,7 @@ export const createSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
@@ -435,8 +427,7 @@ export const createSecrets = async (req: Request, res: Response) => {
); );
if (!hasAccess) { if (!hasAccess) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: message: "You do not have the necessary permission(s) perform this action"
"You do not have the necessary permission(s) perform this action",
}); });
} }
} }
@@ -449,28 +440,27 @@ export const createSecrets = async (req: Request, res: Response) => {
// case: create 1 secret // case: create 1 secret
listOfSecretsToCreate = [req.body.secrets]; listOfSecretsToCreate = [req.body.secrets];
} }
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
secretPath || "/"
);
// in service token when not giving secretpath folderid must be root // in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used // this is to avoid giving folderid when service tokens are used
if ( if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
if (secretPath) { if (secretPath) {
folderId = await getFolderIdFromServiceToken( folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
} }
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
type secretsToCreateType = { type secretsToCreateType = {
@@ -502,15 +492,14 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
tags, tags
}: secretsToCreateType) => { }: secretsToCreateType) => {
let secretBlindIndex; let secretBlindIndex;
if (secretName) { if (secretName) {
secretBlindIndex = secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
await SecretService.generateSecretBlindIndexWithSalt({ secretName,
secretName, salt
salt, });
});
} }
return { return {
@@ -532,22 +521,22 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentTag, secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags, tags
}; };
} }
) )
); );
const newlyCreatedSecrets: ISecret[] = ( const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map(
await Secret.insertMany(secretsToInsert) (insertedSecret) => insertedSecret.toObject()
).map((insertedSecret) => insertedSecret.toObject()); );
setTimeout(async () => { setTimeout(async () => {
// trigger event - push secrets // trigger event - push secrets
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}), })
}); });
}, 5000); }, 5000);
@@ -567,7 +556,7 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag
}) => }) =>
new SecretVersion({ new SecretVersion({
secret: _id, secret: _id,
@@ -586,9 +575,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}) })
), )
}); });
const addAction = await EELogService.createAction({ const addAction = await EELogService.createAction({
@@ -597,7 +586,7 @@ export const createSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id), secretIds: newlyCreatedSecrets.map((n) => n._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -609,14 +598,14 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction], actions: [addAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId, folderId
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -624,7 +613,7 @@ export const createSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets added", event: "secrets added",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: listOfSecretsToCreate.length, numberOfSecrets: listOfSecretsToCreate.length,
@@ -632,13 +621,13 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel: channel, channel: channel,
folderId, folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets: newlyCreatedSecrets, secrets: newlyCreatedSecrets
}); });
}; };
@@ -696,10 +685,7 @@ export const getSecrets = async (req: Request, res: Response) => {
const environment = req.query.environment as string; const environment = req.query.environment as string;
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if ( if ((!folders && folderId && folderId !== "root") || (!folders && secretPath)) {
(!folders && folderId && folderId !== "root") ||
(!folders && secretPath)
) {
res.send({ secrets: [] }); res.send({ secrets: [] });
return; return;
} }
@@ -712,13 +698,15 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
(secretPath as string) || "/"
);
// in service token when not giving secretpath folderid must be root // in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used // this is to avoid giving folderid when service tokens are used
if ( if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
@@ -738,8 +726,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 // query tags table to get all tags ids for the tag names for the given workspace
let tagIds = []; let tagIds = [];
const tagNamesList = const tagNamesList = typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
if (tagNamesList != undefined && tagNamesList.length != 0) { if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId }); const workspaceFromDB = await Tag.find({ workspace: workspaceId });
tagIds = _.map(tagNamesList, (tagName: string) => { tagIds = _.map(tagNamesList, (tagName: string) => {
@@ -762,8 +749,7 @@ export const getSecrets = async (req: Request, res: Response) => {
); );
if (hasNoAccess) { if (hasNoAccess) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: message: "You do not have the necessary permission(s) perform this action"
"You do not have the necessary permission(s) perform this action",
}); });
} }
@@ -773,8 +759,8 @@ export const getSecrets = async (req: Request, res: Response) => {
folder: folderId, folder: folderId,
$or: [ $or: [
{ user: req.user._id }, // personal secrets for this user { 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) { if (tagIds.length > 0) {
@@ -801,8 +787,8 @@ export const getSecrets = async (req: Request, res: Response) => {
environment, environment,
$or: [ $or: [
{ user: userId }, // personal secrets for this user { 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) { if (tagIds.length > 0) {
@@ -820,7 +806,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folder: folderId, folder: folderId,
user: { $exists: false }, // shared secrets only from workspace user: { $exists: false } // shared secrets only from workspace
}; };
if (tagIds.length > 0) { if (tagIds.length > 0) {
@@ -838,7 +824,7 @@ export const getSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id), secretIds: secrets.map((n: any) => n._id)
}); });
readAction && readAction &&
@@ -849,7 +835,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction], actions: [readAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -857,7 +843,7 @@ export const getSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets pulled", event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
@@ -865,13 +851,13 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel, channel,
folderId, folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets, secrets
}); });
}; };
@@ -925,9 +911,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli";
? "web"
: "cli";
interface PatchSecret { interface PatchSecret {
id: string; id: string;
@@ -943,51 +927,47 @@ export const updateSecrets = async (req: Request, res: Response) => {
tags: string[]; tags: string[];
} }
const updateOperationsToPerform = req.body.secrets.map( const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
(secret: PatchSecret) => { const {
const { secretKeyCiphertext,
secretKeyCiphertext, secretKeyIV,
secretKeyIV, secretKeyTag,
secretKeyTag, secretValueCiphertext,
secretValueCiphertext, secretValueIV,
secretValueIV, secretValueTag,
secretValueTag, secretCommentCiphertext,
secretCommentCiphertext, secretCommentIV,
secretCommentIV, secretCommentTag,
secretCommentTag, tags
tags, } = secret;
} = secret;
return { return {
updateOne: { updateOne: {
filter: { _id: new Types.ObjectId(secret.id) }, filter: { _id: new Types.ObjectId(secret.id) },
update: { update: {
$inc: { $inc: {
version: 1, 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,
}
: {}),
}, },
}, 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); await Secret.bulkWrite(updateOperationsToPerform);
@@ -1009,7 +989,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
tags, tags
} = secretModificationsBySecretId[secret._id.toString()]; } = secretModificationsBySecretId[secret._id.toString()];
return { return {
@@ -1018,9 +998,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspace: secret.workspace, workspace: secret.workspace,
type: secret.type, type: secret.type,
environment: secret.environment, environment: secret.environment,
secretKeyCiphertext: secretKeyCiphertext secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
? secretKeyCiphertext
: secret.secretKeyCiphertext,
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV, secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag, secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
secretValueCiphertext: secretValueCiphertext secretValueCiphertext: secretValueCiphertext
@@ -1031,17 +1009,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext secretCommentCiphertext: secretCommentCiphertext
? secretCommentCiphertext ? secretCommentCiphertext
: secret.secretCommentCiphertext, : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
? secretCommentIV secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
: secret.secretCommentIV,
secretCommentTag: secretCommentTag
? secretCommentTag
: secret.secretCommentTag,
tags: tags ? tags : secret.tags, tags: tags ? tags : secret.tags,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}; };
}), })
}; };
await EESecretService.addSecretVersions(secretVersions); await EESecretService.addSecretVersions(secretVersions);
@@ -1062,8 +1036,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
setTimeout(async () => { setTimeout(async () => {
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key)
}), })
}); });
}, 10000); }, 10000);
@@ -1073,7 +1047,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -1085,7 +1059,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [updateAction], actions: [updateAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
@@ -1101,15 +1075,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets modified", event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
workspaceId: key, workspaceId: key,
channel: channel, channel: channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
}); });
@@ -1117,9 +1091,9 @@ export const updateSecrets = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
secrets: await Secret.find({ secrets: await Secret.find({
_id: { _id: {
$in: req.secrets.map((secret: ISecret) => secret._id), $in: req.secrets.map((secret: ISecret) => secret._id)
}, }
}), })
}); });
}; };
@@ -1179,12 +1153,12 @@ export const deleteSecrets = async (req: Request, res: Response) => {
await Secret.deleteMany({ await Secret.deleteMany({
_id: { _id: {
$in: toDelete, $in: toDelete
}, }
}); });
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: toDelete, secretIds: toDelete
}); });
// group secrets into workspaces so deleted secrets can // group secrets into workspaces so deleted secrets can
@@ -1202,8 +1176,8 @@ export const deleteSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets // trigger event - push secrets
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key)
}), })
}); });
const deleteAction = await EELogService.createAction({ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
@@ -1211,7 +1185,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -1223,7 +1197,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [deleteAction], actions: [deleteAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
@@ -1237,20 +1211,20 @@ export const deleteSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets deleted", event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
workspaceId: key, workspaceId: key,
channel: channel, channel: channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
}); });
return res.status(200).send({ return res.status(200).send({
secrets: req.secrets, secrets: req.secrets
}); });
}; };
@@ -2,10 +2,7 @@ import { Request, Response } from "express";
import crypto from "crypto"; import crypto from "crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { ServiceAccount, ServiceTokenData, User } from "../../models"; import { ServiceAccount, ServiceTokenData, User } from "../../models";
import { import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
} from "../../variables";
import { getSaltRounds } from "../../config"; import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import Folder from "../../models/folder"; import Folder from "../../models/folder";
@@ -46,14 +43,13 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
if (!(req.authData.authPayload instanceof ServiceTokenData)) if (!(req.authData.authPayload instanceof ServiceTokenData))
throw BadRequestError({ 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( const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
req.authData.authPayload._id
)
.select("+encryptedKey +iv +tag") .select("+encryptedKey +iv +tag")
.populate("user").lean(); .populate("user")
.lean();
return res.status(200).json(serviceTokenData); 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) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData; let serviceTokenData;
const { const { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body;
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 secret = crypto.randomBytes(16).toString("hex"); const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
@@ -103,10 +77,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
let user, serviceAccount; let user, serviceAccount;
if ( if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
req.authData.authMode === AUTH_MODE_JWT &&
req.authData.authPayload instanceof User
) {
user = req.authData.authPayload._id; user = req.authData.authPayload._id;
} }
@@ -120,17 +91,16 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
serviceTokenData = await new ServiceTokenData({ serviceTokenData = await new ServiceTokenData({
name, name,
workspace: workspaceId, workspace: workspaceId,
environment,
user, user,
serviceAccount, serviceAccount,
scopes,
lastUsed: new Date(), lastUsed: new Date(),
expiresAt, expiresAt,
secretHash, secretHash,
encryptedKey, encryptedKey,
iv, iv,
tag, tag,
secretPath, permissions
permissions,
}).save(); }).save();
// return service token data without sensitive data // return service token data without sensitive data
@@ -142,7 +112,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
serviceToken, serviceToken,
serviceTokenData, serviceTokenData
}); });
}; };
@@ -155,11 +125,9 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
export const deleteServiceTokenData = async (req: Request, res: Response) => { export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params; const { serviceTokenDataId } = req.params;
const serviceTokenData = await ServiceTokenData.findByIdAndDelete( const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
serviceTokenDataId
);
return res.status(200).send({ return res.status(200).send({
serviceTokenData, serviceTokenData
}); });
}; };
+143 -171
View File
@@ -4,13 +4,14 @@ import {
DeleteSecretParams, DeleteSecretParams,
GetSecretParams, GetSecretParams,
GetSecretsParams, GetSecretsParams,
UpdateSecretParams, UpdateSecretParams
} from "../interfaces/services/SecretService"; } from "../interfaces/services/SecretService";
import { import {
ISecret, ISecret,
IServiceTokenData,
Secret, Secret,
SecretBlindIndexData, SecretBlindIndexData,
ServiceTokenData, ServiceTokenData
} from "../models"; } from "../models";
import { SecretVersion } from "../ee/models"; import { SecretVersion } from "../ee/models";
import { import {
@@ -18,7 +19,7 @@ import {
InternalServerError, InternalServerError,
SecretBlindIndexDataNotFoundError, SecretBlindIndexDataNotFoundError,
SecretNotFoundError, SecretNotFoundError,
UnauthorizedRequestError, UnauthorizedRequestError
} from "../utils/errors"; } from "../utils/errors";
import { import {
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
@@ -29,51 +30,57 @@ import {
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../variables"; } from "../variables";
import crypto from "crypto"; import crypto from "crypto";
import * as argon2 from "argon2"; import * as argon2 from "argon2";
import { import {
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8
} from "../utils/crypto"; } from "../utils/crypto";
import { TelemetryService } from "../services"; import { TelemetryService } from "../services";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EELogService, EESecretService } from "../ee/services"; import { EELogService, EESecretService } from "../ee/services";
import { import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj,
} from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService"; import { getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
export const isValidScope = (
authPayload: IServiceTokenData,
environment: string,
secretPath: string
) => {
const { scopes: tkScopes } = authPayload;
const validScope = tkScopes.find(
(scope) =>
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
scope.environment === environment
);
return Boolean(validScope);
};
/** /**
* Returns an object containing secret [secret] but with its value, key, comment decrypted. * Returns an object containing secret [secret] but with its value, key, comment decrypted.
* *
* Precondition: the workspace for secret [secret] must have E2EE disabled * Precondition: the workspace for secret [secret] must have E2EE disabled
* @param {ISecret} secret - secret to repackage to raw * @param {ISecret} secret - secret to repackage to raw
* @param {String} key - symmetric key to use to decrypt secret * @param {String} key - symmetric key to use to decrypt secret
* @returns * @returns
*/ */
export const repackageSecretToRaw = ({ export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
secret,
key,
}: {
secret: ISecret;
key: string;
}) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({ const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
tag: secret.secretKeyTag, tag: secret.secretKeyTag,
key, key
}); });
const secretValue = decryptSymmetric128BitHexKeyUTF8({ const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext, ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV, iv: secret.secretValueIV,
tag: secret.secretValueTag, tag: secret.secretValueTag,
key, key
}); });
let secretComment = ""; let secretComment = "";
@@ -83,11 +90,11 @@ export const repackageSecretToRaw = ({
ciphertext: secret.secretCommentCiphertext, ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV, iv: secret.secretCommentIV,
tag: secret.secretCommentTag, tag: secret.secretCommentTag,
key, key
}); });
} }
return ({ return {
_id: secret._id, _id: secret._id,
version: secret.version, version: secret.version,
workspace: secret.workspace, workspace: secret.workspace,
@@ -96,9 +103,9 @@ export const repackageSecretToRaw = ({
user: secret.user, user: secret.user,
secretKey, secretKey,
secretValue, secretValue,
secretComment, secretComment
}); };
} };
/** /**
* Create secret blind index data containing encrypted blind index [salt] * Create secret blind index data containing encrypted blind index [salt]
@@ -107,7 +114,7 @@ export const repackageSecretToRaw = ({
* @param {Types.ObjectId} obj.workspaceId * @param {Types.ObjectId} obj.workspaceId
*/ */
export const createSecretBlindIndexDataHelper = async ({ export const createSecretBlindIndexDataHelper = async ({
workspaceId, workspaceId
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -121,7 +128,7 @@ export const createSecretBlindIndexDataHelper = async ({
const { const {
ciphertext: encryptedSaltCiphertext, ciphertext: encryptedSaltCiphertext,
iv: saltIV, iv: saltIV,
tag: saltTag, tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey); } = client.encryptSymmetric(salt, rootEncryptionKey);
return await new SecretBlindIndexData({ return await new SecretBlindIndexData({
@@ -130,16 +137,16 @@ export const createSecretBlindIndexDataHelper = async ({
saltIV, saltIV,
saltTag, saltTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64, keyEncoding: ENCODING_SCHEME_BASE64
}).save(); }).save();
} else { } else {
const { const {
ciphertext: encryptedSaltCiphertext, ciphertext: encryptedSaltCiphertext,
iv: saltIV, iv: saltIV,
tag: saltTag, tag: saltTag
} = encryptSymmetric128BitHexKeyUTF8({ } = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt, plaintext: salt,
key: encryptionKey, key: encryptionKey
}); });
return await new SecretBlindIndexData({ return await new SecretBlindIndexData({
@@ -148,7 +155,7 @@ export const createSecretBlindIndexDataHelper = async ({
saltIV, saltIV,
saltTag, saltTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}).save(); }).save();
} }
}; };
@@ -160,7 +167,7 @@ export const createSecretBlindIndexDataHelper = async ({
* @returns * @returns
*/ */
export const getSecretBlindIndexSaltHelper = async ({ export const getSecretBlindIndexSaltHelper = async ({
workspaceId, workspaceId
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -168,36 +175,30 @@ export const getSecretBlindIndexSaltHelper = async ({
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId
}).select("+algorithm +keyEncoding"); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
if ( if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
rootEncryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
) {
return client.decryptSymmetric( return client.decryptSymmetric(
secretBlindIndexData.encryptedSaltCiphertext, secretBlindIndexData.encryptedSaltCiphertext,
rootEncryptionKey, rootEncryptionKey,
secretBlindIndexData.saltIV, secretBlindIndexData.saltIV,
secretBlindIndexData.saltTag secretBlindIndexData.saltTag
); );
} else if ( } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
encryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
) {
// decrypt workspace salt // decrypt workspace salt
return decryptSymmetric128BitHexKeyUTF8({ return decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext, ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV, iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag, tag: secretBlindIndexData.saltTag,
key: encryptionKey, key: encryptionKey
}); });
} }
throw InternalServerError({ 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 +211,7 @@ export const getSecretBlindIndexSaltHelper = async ({
*/ */
export const generateSecretBlindIndexWithSaltHelper = async ({ export const generateSecretBlindIndexWithSaltHelper = async ({
secretName, secretName,
salt, salt
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
@@ -224,7 +225,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
memoryCost: 65536, // default pool of 64 MiB per thread. memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32, hashLength: 32,
parallelism: 1, parallelism: 1,
raw: true, raw: true
}) })
).toString("base64"); ).toString("base64");
@@ -240,7 +241,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
*/ */
export const generateSecretBlindIndexHelper = async ({ export const generateSecretBlindIndexHelper = async ({
secretName, secretName,
workspaceId, workspaceId
}: { }: {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
@@ -250,16 +251,13 @@ export const generateSecretBlindIndexHelper = async ({
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId
}).select("+algorithm +keyEncoding"); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
let salt; let salt;
if ( if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
rootEncryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
) {
salt = client.decryptSymmetric( salt = client.decryptSymmetric(
secretBlindIndexData.encryptedSaltCiphertext, secretBlindIndexData.encryptedSaltCiphertext,
rootEncryptionKey, rootEncryptionKey,
@@ -269,32 +267,29 @@ export const generateSecretBlindIndexHelper = async ({
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName, secretName,
salt, salt
}); });
return secretBlindIndex; return secretBlindIndex;
} else if ( } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
encryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
) {
// decrypt workspace salt // decrypt workspace salt
salt = decryptSymmetric128BitHexKeyUTF8({ salt = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext, ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV, iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag, tag: secretBlindIndexData.saltTag,
key: encryptionKey, key: encryptionKey
}); });
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName, secretName,
salt, salt
}); });
return secretBlindIndex; return secretBlindIndex;
} }
throw InternalServerError({ throw InternalServerError({
message: "Failed to generate secret blind index", message: "Failed to generate secret blind index"
}); });
}; };
@@ -323,38 +318,32 @@ export const createSecretHelper = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretPath = "/"
}: CreateSecretParams) => { }: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
const exists = await Secret.exists({ const exists = await Secret.exists({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
folder: folderId, folder: folderId,
type, type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}); });
if (exists) if (exists)
throw BadRequestError({ throw BadRequestError({
message: "Failed to create secret that already exists", message: "Failed to create secret that already exists"
}); });
if (type === SECRET_PERSONAL) { if (type === SECRET_PERSONAL) {
@@ -365,13 +354,12 @@ export const createSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
folder: folderId, folder: folderId,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
type: SECRET_SHARED, type: SECRET_SHARED
}); });
if (!exists) if (!exists)
throw BadRequestError({ throw BadRequestError({
message: message: "Failed to create personal secret override for no corresponding shared secret"
"Failed to create personal secret override for no corresponding shared secret",
}); });
} }
@@ -394,7 +382,7 @@ export const createSecretHelper = async ({
secretCommentTag, secretCommentTag,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}).save(); }).save();
const secretVersion = new SecretVersion({ const secretVersion = new SecretVersion({
@@ -414,12 +402,12 @@ export const createSecretHelper = async ({
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
// (EE) add version for new secret // (EE) add version for new secret
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: [secretVersion], secretVersions: [secretVersion]
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -427,7 +415,7 @@ export const createSecretHelper = async ({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: [secret._id], secretIds: [secret._id]
}); });
action && action &&
@@ -436,14 +424,14 @@ export const createSecretHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId,
environment, environment,
folderId, folderId
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -452,7 +440,7 @@ export const createSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets added", event: "secrets added",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
@@ -460,8 +448,8 @@ export const createSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -480,21 +468,16 @@ export const getSecretsHelper = async ({
workspaceId, workspaceId,
environment, environment,
authData, authData,
secretPath = "/", secretPath = "/"
}: GetSecretsParams) => { }: GetSecretsParams) => {
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
// get personal secrets first // get personal secrets first
secrets = await Secret.find({ secrets = await Secret.find({
@@ -502,8 +485,10 @@ export const getSecretsHelper = async ({
environment, environment,
folder: folderId, folder: folderId,
type: SECRET_PERSONAL, type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData)
}).populate("tags").lean(); })
.populate("tags")
.lean();
// concat with shared secrets // concat with shared secrets
secrets = secrets.concat( secrets = secrets.concat(
@@ -513,9 +498,11 @@ export const getSecretsHelper = async ({
folder: folderId, folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED,
secretBlindIndex: { secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex), $nin: secrets.map((secret) => secret.secretBlindIndex)
}, }
}).populate("tags").lean() })
.populate("tags")
.lean()
); );
// (EE) create (audit) log // (EE) create (audit) log
@@ -523,7 +510,7 @@ export const getSecretsHelper = async ({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: secrets.map((secret) => secret._id), secretIds: secrets.map((secret) => secret._id)
}); });
action && action &&
@@ -532,7 +519,7 @@ export const getSecretsHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -541,7 +528,7 @@ export const getSecretsHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets pulled", event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
@@ -549,8 +536,8 @@ export const getSecretsHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -573,25 +560,20 @@ export const getSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/", secretPath = "/"
}: GetSecretParams) => { }: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
let secret: ISecret | null = null; let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
// try getting personal secret first (if exists) // try getting personal secret first (if exists)
secret = await Secret.findOne({ secret = await Secret.findOne({
@@ -600,7 +582,7 @@ export const getSecretHelper = async ({
environment, environment,
folder: folderId, folder: folderId,
type: type ?? SECRET_PERSONAL, type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}).lean(); }).lean();
if (!secret) { if (!secret) {
@@ -611,7 +593,7 @@ export const getSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED
}).lean(); }).lean();
} }
@@ -622,7 +604,7 @@ export const getSecretHelper = async ({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: [secret._id], secretIds: [secret._id]
}); });
action && action &&
@@ -631,7 +613,7 @@ export const getSecretHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -640,7 +622,7 @@ export const getSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets pull", event: "secrets pull",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
@@ -648,8 +630,8 @@ export const getSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -679,26 +661,21 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath, secretPath
}: UpdateSecretParams) => { }: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
let secret: ISecret | null = null; let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
if (type === SECRET_SHARED) { if (type === SECRET_SHARED) {
// case: update shared secret // case: update shared secret
@@ -708,16 +685,16 @@ export const updateSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId,
type, type
}, },
{ {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
$inc: { version: 1 }, $inc: { version: 1 }
}, },
{ {
new: true, new: true
} }
); );
} else { } else {
@@ -730,16 +707,16 @@ export const updateSecretHelper = async ({
environment, environment,
type, type,
folder: folderId, folder: folderId,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData)
}, },
{ {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
$inc: { version: 1 }, $inc: { version: 1 }
}, },
{ {
new: true, new: true
} }
); );
} }
@@ -763,12 +740,12 @@ export const updateSecretHelper = async ({
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
// (EE) add version for new secret // (EE) add version for new secret
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: [secretVersion], secretVersions: [secretVersion]
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -776,7 +753,7 @@ export const updateSecretHelper = async ({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: [secret._id], secretIds: [secret._id]
}); });
action && action &&
@@ -785,14 +762,14 @@ export const updateSecretHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId,
environment, environment,
folderId: secret?.folder, folderId: secret?.folder
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -801,7 +778,7 @@ export const updateSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets modified", event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
@@ -809,8 +786,8 @@ export const updateSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -833,26 +810,20 @@ export const deleteSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/", secretPath = "/"
}: DeleteSecretParams) => { }: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
let secret: ISecret | null = null; let secret: ISecret | null = null;
@@ -862,7 +833,7 @@ export const deleteSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId
}).lean(); }).lean();
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
@@ -870,14 +841,14 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
folder: folderId, folder: folderId
}).lean(); }).lean();
await Secret.deleteMany({ await Secret.deleteMany({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId
}); });
} else { } else {
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
@@ -886,7 +857,7 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData)
}).lean(); }).lean();
if (secret) { if (secret) {
@@ -897,7 +868,7 @@ export const deleteSecretHelper = async ({
if (!secret) throw SecretNotFoundError(); if (!secret) throw SecretNotFoundError();
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: secrets.map((secret) => secret._id), secretIds: secrets.map((secret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -905,22 +876,23 @@ export const deleteSecretHelper = async ({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: secrets.map((secret) => secret._id), secretIds: secrets.map((secret) => secret._id)
}); });
action && (await EELogService.createLog({ action &&
...getAuthDataPayloadIdObj(authData), (await EELogService.createLog({
workspaceId, ...getAuthDataPayloadIdObj(authData),
actions: [action], workspaceId,
channel: authData.authChannel, actions: [action],
ipAddress: authData.authIP, channel: authData.authChannel,
})); ipAddress: authData.authIP
}));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId,
environment, environment,
folderId: secret?.folder, folderId: secret?.folder
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -929,7 +901,7 @@ export const deleteSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets deleted", event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
@@ -937,13 +909,13 @@ export const deleteSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
return ({ return {
secrets, secrets,
secret, secret
}); };
}; };
+33 -27
View File
@@ -4,7 +4,10 @@ export interface IServiceTokenData extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; scopes: Array<{
environment: string;
secretPath: string;
}>;
user: Types.ObjectId; user: Types.ObjectId;
serviceAccount: Types.ObjectId; serviceAccount: Types.ObjectId;
lastUsed: Date; lastUsed: Date;
@@ -13,7 +16,6 @@ export interface IServiceTokenData extends Document {
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
secretPath: string;
permissions: string[]; permissions: string[];
} }
@@ -21,68 +23,72 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{ {
name: { name: {
type: String, type: String,
required: true, required: true
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
environment: { scopes: {
type: String, type: [
required: true, {
environment: {
type: String,
required: true
},
secretPath: {
type: String,
default: "/",
required: true
}
}
],
required: true
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User",
required: true, required: true
}, },
serviceAccount: { serviceAccount: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "ServiceAccount", ref: "ServiceAccount"
}, },
lastUsed: { lastUsed: {
type: Date, type: Date
}, },
expiresAt: { expiresAt: {
type: Date, type: Date
}, },
secretHash: { secretHash: {
type: String, type: String,
required: true, required: true,
select: false, select: false
}, },
encryptedKey: { encryptedKey: {
type: String, type: String,
select: false, select: false
}, },
iv: { iv: {
type: String, type: String,
select: false, select: false
}, },
tag: { tag: {
type: String, type: String,
select: false, select: false
}, },
permissions: { permissions: {
type: [String], type: [String],
enum: ["read", "write"], enum: ["read", "write"],
default: ["read"], default: ["read"]
}, }
secretPath: {
type: String,
default: "/",
required: true,
},
}, },
{ {
timestamps: true, timestamps: true
} }
); );
const ServiceTokenData = model<IServiceTokenData>( const ServiceTokenData = model<IServiceTokenData>("ServiceTokenData", serviceTokenDataSchema);
"ServiceTokenData",
serviceTokenDataSchema
);
export default ServiceTokenData; export default ServiceTokenData;
+12 -15
View File
@@ -4,7 +4,7 @@ import {
requireAuth, requireAuth,
requireServiceTokenDataAuth, requireServiceTokenDataAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest, validateRequest
} from "../../middleware"; } from "../../middleware";
import { body, param } from "express-validator"; import { body, param } from "express-validator";
import { import {
@@ -13,14 +13,14 @@ import {
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
MEMBER, MEMBER,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS
} from "../../variables"; } from "../../variables";
import { serviceTokenDataController } from "../../controllers/v2"; import { serviceTokenDataController } from "../../controllers/v2";
router.get( router.get(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN], acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN]
}), }),
serviceTokenDataController.getServiceTokenData serviceTokenDataController.getServiceTokenData
); );
@@ -28,33 +28,30 @@ router.get(
router.post( router.post(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body", locationWorkspaceId: "body",
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
body("name").exists().isString().trim(), body("name").exists().isString().trim(),
body("workspaceId").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("encryptedKey").exists().isString().trim(),
body("iv").exists().isString().trim(), body("iv").exists().isString().trim(),
body("secretPath").isString().default("/").trim(),
body("tag").exists().isString().trim(), body("tag").exists().isString().trim(),
body("expiresIn").exists().isNumeric(), // measured in ms body("expiresIn").exists().isNumeric(), // measured in ms
body("permissions") body("permissions")
.isArray({ min: 1 }) .isArray({ min: 1 })
.custom((value: string[]) => { .custom((value: string[]) => {
const allowedPermissions = ["read", "write"]; const allowedPermissions = ["read", "write"];
const invalidValues = value.filter( const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
(v) => !allowedPermissions.includes(v)
);
if (invalidValues.length > 0) { if (invalidValues.length > 0) {
throw new Error( throw new Error(`permissions contains invalid values: ${invalidValues.join(", ")}`);
`permissions contains invalid values: ${invalidValues.join(", ")}`
);
} }
return true; return true;
@@ -66,10 +63,10 @@ router.post(
router.delete( router.delete(
"/:serviceTokenDataId", "/:serviceTokenDataId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT], acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireServiceTokenDataAuth({ requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
}), }),
param("serviceTokenDataId").exists().trim(), param("serviceTokenDataId").exists().trim(),
validateRequest, validateRequest,
+102 -91
View File
@@ -13,14 +13,14 @@ import {
Secret, Secret,
SecretBlindIndexData, SecretBlindIndexData,
ServiceTokenData, ServiceTokenData,
Workspace, Workspace
} from "../../models"; } from "../../models";
import { generateKeyPair } from "../../utils/crypto"; import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8
} from "../../variables"; } from "../../variables";
import { InternalServerError } from "../errors"; import { InternalServerError } from "../errors";
@@ -29,10 +29,7 @@ import { InternalServerError } from "../errors";
* corresponding secret versions * corresponding secret versions
*/ */
export const backfillSecretVersions = async () => { export const backfillSecretVersions = async () => {
await Secret.updateMany( await Secret.updateMany({ version: { $exists: false } }, { $set: { version: 1 } });
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([ const unversionedSecrets: ISecret[] = await Secret.aggregate([
{ {
@@ -40,14 +37,14 @@ export const backfillSecretVersions = async () => {
from: "secretversions", from: "secretversions",
localField: "_id", localField: "_id",
foreignField: "secret", foreignField: "secret",
as: "versions", as: "versions"
}, }
}, },
{ {
$match: { $match: {
versions: { $size: 0 }, versions: { $size: 0 }
}, }
}, }
]); ]);
if (unversionedSecrets.length > 0) { if (unversionedSecrets.length > 0) {
@@ -62,9 +59,9 @@ export const backfillSecretVersions = async () => {
workspace: s.workspace, workspace: s.workspace,
environment: s.environment, environment: s.environment,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}) })
), )
}); });
} }
console.log("Migration: Secret version migration v1 complete"); console.log("Migration: Secret version migration v1 complete");
@@ -80,8 +77,8 @@ export const backfillBots = async () => {
const workspaceIdsWithBot = await Bot.distinct("workspace"); const workspaceIdsWithBot = await Bot.distinct("workspace");
const workspaceIdsToAddBot = await Workspace.distinct("_id", { const workspaceIdsToAddBot = await Workspace.distinct("_id", {
_id: { _id: {
$nin: workspaceIdsWithBot, $nin: workspaceIdsWithBot
}, }
}); });
if (workspaceIdsToAddBot.length === 0) return; if (workspaceIdsToAddBot.length === 0) return;
@@ -94,7 +91,7 @@ export const backfillBots = async () => {
const { const {
ciphertext: encryptedPrivateKey, ciphertext: encryptedPrivateKey,
iv, iv,
tag, tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey); } = client.encryptSymmetric(privateKey, rootEncryptionKey);
return new Bot({ return new Bot({
@@ -106,16 +103,16 @@ export const backfillBots = async () => {
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64, keyEncoding: ENCODING_SCHEME_BASE64
}); });
} else if (encryptionKey) { } else if (encryptionKey) {
const { const {
ciphertext: encryptedPrivateKey, ciphertext: encryptedPrivateKey,
iv, iv,
tag, tag
} = encryptSymmetric128BitHexKeyUTF8({ } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey, plaintext: privateKey,
key: encryptionKey, key: encryptionKey
}); });
return new Bot({ return new Bot({
@@ -127,13 +124,12 @@ export const backfillBots = async () => {
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
} }
throw InternalServerError({ throw InternalServerError({
message: message: "Failed to backfill workspace bots due to missing encryption key"
"Failed to backfill workspace bots due to missing encryption key",
}); });
}) })
); );
@@ -149,13 +145,11 @@ export const backfillSecretBlindIndexData = async () => {
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct( const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct("workspace");
"workspace"
);
const workspaceIdsToBlindIndex = await Workspace.distinct("_id", { const workspaceIdsToBlindIndex = await Workspace.distinct("_id", {
_id: { _id: {
$nin: workspaceIdsBlindIndexed, $nin: workspaceIdsBlindIndexed
}, }
}); });
if (workspaceIdsToBlindIndex.length === 0) return; if (workspaceIdsToBlindIndex.length === 0) return;
@@ -168,7 +162,7 @@ export const backfillSecretBlindIndexData = async () => {
const { const {
ciphertext: encryptedSaltCiphertext, ciphertext: encryptedSaltCiphertext,
iv: saltIV, iv: saltIV,
tag: saltTag, tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey); } = client.encryptSymmetric(salt, rootEncryptionKey);
return new SecretBlindIndexData({ return new SecretBlindIndexData({
@@ -177,16 +171,16 @@ export const backfillSecretBlindIndexData = async () => {
saltIV, saltIV,
saltTag, saltTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64, keyEncoding: ENCODING_SCHEME_BASE64
}); });
} else if (encryptionKey) { } else if (encryptionKey) {
const { const {
ciphertext: encryptedSaltCiphertext, ciphertext: encryptedSaltCiphertext,
iv: saltIV, iv: saltIV,
tag: saltTag, tag: saltTag
} = encryptSymmetric128BitHexKeyUTF8({ } = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt, plaintext: salt,
key: encryptionKey, key: encryptionKey
}); });
return new SecretBlindIndexData({ return new SecretBlindIndexData({
@@ -195,13 +189,12 @@ export const backfillSecretBlindIndexData = async () => {
saltIV, saltIV,
saltTag, saltTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
} }
throw InternalServerError({ throw InternalServerError({
message: message: "Failed to backfill secret blind index data due to missing encryption key"
"Failed to backfill secret blind index data due to missing encryption key",
}); });
}) })
); );
@@ -219,17 +212,17 @@ export const backfillEncryptionMetadata = async () => {
await Secret.updateMany( await Secret.updateMany(
{ {
algorithm: { algorithm: {
$exists: false, $exists: false
}, },
keyEncoding: { keyEncoding: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}, }
} }
); );
@@ -237,17 +230,17 @@ export const backfillEncryptionMetadata = async () => {
await SecretVersion.updateMany( await SecretVersion.updateMany(
{ {
algorithm: { algorithm: {
$exists: false, $exists: false
}, },
keyEncoding: { keyEncoding: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}, }
} }
); );
@@ -255,17 +248,17 @@ export const backfillEncryptionMetadata = async () => {
await SecretBlindIndexData.updateMany( await SecretBlindIndexData.updateMany(
{ {
algorithm: { algorithm: {
$exists: false, $exists: false
}, },
keyEncoding: { keyEncoding: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}, }
} }
); );
@@ -273,17 +266,17 @@ export const backfillEncryptionMetadata = async () => {
await Bot.updateMany( await Bot.updateMany(
{ {
algorithm: { algorithm: {
$exists: false, $exists: false
}, },
keyEncoding: { keyEncoding: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}, }
} }
); );
@@ -291,17 +284,17 @@ export const backfillEncryptionMetadata = async () => {
await BackupPrivateKey.updateMany( await BackupPrivateKey.updateMany(
{ {
algorithm: { algorithm: {
$exists: false, $exists: false
}, },
keyEncoding: { keyEncoding: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}, }
} }
); );
@@ -309,17 +302,17 @@ export const backfillEncryptionMetadata = async () => {
await IntegrationAuth.updateMany( await IntegrationAuth.updateMany(
{ {
algorithm: { algorithm: {
$exists: false, $exists: false
}, },
keyEncoding: { keyEncoding: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}, }
} }
); );
}; };
@@ -328,26 +321,26 @@ export const backfillSecretFolders = async () => {
await Secret.updateMany( await Secret.updateMany(
{ {
folder: { folder: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
folder: "root", folder: "root"
}, }
} }
); );
await SecretVersion.updateMany( await SecretVersion.updateMany(
{ {
folder: { folder: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
folder: "root", folder: "root"
}, }
} }
); );
@@ -355,20 +348,20 @@ export const backfillSecretFolders = async () => {
await SecretVersion.updateMany( await SecretVersion.updateMany(
{ {
tags: { tags: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
tags: [], tags: []
}, }
} }
); );
let secretSnapshots = await SecretSnapshot.find({ let secretSnapshots = await SecretSnapshot.find({
environment: { environment: {
$exists: false, $exists: false
}, }
}) })
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions") .populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
.limit(50); .limit(50);
@@ -377,8 +370,7 @@ export const backfillSecretFolders = async () => {
for (const secSnapshot of secretSnapshots) { for (const secSnapshot of secretSnapshots) {
const groupSnapByEnv: Record<string, Array<ISecretVersion>> = {}; const groupSnapByEnv: Record<string, Array<ISecretVersion>> = {};
secSnapshot.secretVersions.forEach((secVer) => { secSnapshot.secretVersions.forEach((secVer) => {
if (!groupSnapByEnv?.[secVer.environment]) if (!groupSnapByEnv?.[secVer.environment]) groupSnapByEnv[secVer.environment] = [];
groupSnapByEnv[secVer.environment] = [];
groupSnapByEnv[secVer.environment].push(secVer); groupSnapByEnv[secVer.environment].push(secVer);
}); });
@@ -390,7 +382,7 @@ export const backfillSecretFolders = async () => {
...secSnapshot.toObject({ virtuals: false }), ...secSnapshot.toObject({ virtuals: false }),
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
environment: snapEnv, environment: snapEnv,
secretVersions: secretIdsOfEnvGroup, secretVersions: secretIdsOfEnvGroup
}; };
}); });
@@ -400,8 +392,8 @@ export const backfillSecretFolders = async () => {
secretSnapshots = await SecretSnapshot.find({ secretSnapshots = await SecretSnapshot.find({
environment: { environment: {
$exists: false, $exists: false
}, }
}) })
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions") .populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
.limit(50); .limit(50);
@@ -414,13 +406,13 @@ export const backfillServiceToken = async () => {
await ServiceTokenData.updateMany( await ServiceTokenData.updateMany(
{ {
secretPath: { secretPath: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
secretPath: "/", secretPath: "/"
}, }
} }
); );
console.log("Migration: Service token migration v1 complete"); console.log("Migration: Service token migration v1 complete");
@@ -430,14 +422,33 @@ export const backfillIntegration = async () => {
await Integration.updateMany( await Integration.updateMany(
{ {
secretPath: { secretPath: {
$exists: false, $exists: false
}, }
}, },
{ {
$set: { $set: {
secretPath: "/", secretPath: "/"
}, }
} }
); );
console.log("Migration: Integration migration v1 complete"); console.log("Migration: Integration migration v1 complete");
}; };
export const backfillServiceTokenMultiScope = async () => {
await ServiceTokenData.updateMany(
{
scopes: {
$exists: false
}
},
[
{
$set: {
scopes: [{ environment: "$environment", secretPath: "$secretPath" }]
}
}
]
);
console.log("Migration: Service token migration v2 complete");
};
+5 -6
View File
@@ -14,17 +14,15 @@ import {
backfillSecretFolders, backfillSecretFolders,
backfillSecretVersions, backfillSecretVersions,
backfillServiceToken, backfillServiceToken,
backfillServiceTokenMultiScope
} from "./backfillData"; } from "./backfillData";
import { import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData";
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts,
} from "./reencryptData";
import { import {
getClientIdGoogle, getClientIdGoogle,
getClientSecretGoogle, getClientSecretGoogle,
getMongoURL, getMongoURL,
getNodeEnv, getNodeEnv,
getSentryDSN, getSentryDSN
} from "../../config"; } from "../../config";
import { initializePassport } from "../auth"; import { initializePassport } from "../auth";
@@ -79,6 +77,7 @@ export const setup = async () => {
await backfillSecretFolders(); await backfillSecretFolders();
await backfillServiceToken(); await backfillServiceToken();
await backfillIntegration(); await backfillIntegration();
await backfillServiceTokenMultiScope();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY
@@ -90,7 +89,7 @@ export const setup = async () => {
dsn: await getSentryDSN(), dsn: await getSentryDSN(),
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === "production" ? false : true, debug: (await getNodeEnv()) === "production" ? false : true,
environment: await getNodeEnv(), environment: await getNodeEnv()
}); });
await createTestUserForDevelopment(); await createTestUserForDevelopment();
+126 -124
View File
@@ -1,22 +1,19 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
ISecret, ISecret,
IServiceAccount, IServiceAccount,
IServiceTokenData, IServiceTokenData,
IUser, IUser,
ServiceAccount, ServiceAccount,
ServiceTokenData, ServiceTokenData,
User, User
} from "../models"; } from "../models";
import { import { ServiceTokenDataNotFoundError, UnauthorizedRequestError } from "../utils/errors";
ServiceTokenDataNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
import { import {
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN
} from "../variables"; } from "../variables";
import { validateUserClientForWorkspace } from "./user"; import { validateUserClientForWorkspace } from "./user";
import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
@@ -30,65 +27,71 @@ import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/ */
export const validateClientForServiceTokenData = async ({ export const validateClientForServiceTokenData = async ({
authData, authData,
serviceTokenDataId, serviceTokenDataId,
acceptedRoles, acceptedRoles
}: { }: {
authData: { authData: {
authMode: string; authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData; authPayload: IUser | IServiceAccount | IServiceTokenData;
}; };
serviceTokenDataId: Types.ObjectId; serviceTokenDataId: Types.ObjectId;
acceptedRoles: Array<"admin" | "member">; acceptedRoles: Array<"admin" | "member">;
}) => { }) => {
const serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId)
.findById(serviceTokenDataId) .select("+encryptedKey +iv +tag")
.select("+encryptedKey +iv +tag") .populate<{ user: IUser }>("user");
.populate<{ user: IUser }>("user");
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ if (!serviceTokenData)
message: "Failed to find service token data", throw ServiceTokenDataNotFoundError({
message: "Failed to find service token data"
}); });
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({ await validateUserClientForWorkspace({
user: authData.authPayload, user: authData.authPayload,
workspaceId: serviceTokenData.workspace, workspaceId: serviceTokenData.workspace,
acceptedRoles, acceptedRoles
}); });
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { return serviceTokenData;
await validateServiceAccountClientForWorkspace({ }
serviceAccount: authData.authPayload,
workspaceId: serviceTokenData.workspace,
});
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { if (
throw UnauthorizedRequestError({ authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
message: "Failed service token authorization for service token data", authData.authPayload instanceof ServiceAccount
}); ) {
} await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: serviceTokenData.workspace
});
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { return serviceTokenData;
await validateUserClientForWorkspace({ }
user: authData.authPayload,
workspaceId: serviceTokenData.workspace, if (
acceptedRoles, authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
}); authData.authPayload instanceof ServiceTokenData
) {
return serviceTokenData;
}
throw UnauthorizedRequestError({ 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 * 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 * @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/ */
export const validateServiceTokenDataClientForWorkspace = async ({ export const validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData, serviceTokenData,
workspaceId, workspaceId,
environment, environment,
requiredPermissions, requiredPermissions
}: { }: {
serviceTokenData: IServiceTokenData; serviceTokenData: IServiceTokenData;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
requiredPermissions?: string[]; requiredPermissions?: string[];
}) => { }) => {
if (!serviceTokenData.workspace.equals(workspaceId)) { if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed // 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({ 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 * 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 * @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/ */
export const validateServiceTokenDataClientForSecrets = async ({ export const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData, serviceTokenData,
secrets, secrets,
requiredPermissions, requiredPermissions
}: { }: {
serviceTokenData: IServiceTokenData; serviceTokenData: IServiceTokenData;
secrets: ISecret[]; secrets: ISecret[];
requiredPermissions?: string[]; 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.scopes.find(({ environment: tkEnv }) => tkEnv === secret.environment)) {
if (!serviceTokenData.workspace.equals(secret.workspace)) { // case: invalid environment passed
// case: invalid workspaceId passed throw UnauthorizedRequestError({
throw UnauthorizedRequestError({ message: "Failed service token authorization for the given workspace environment"
message: "Failed service token authorization for the given workspace", });
}); }
}
requiredPermissions?.forEach((permission) => {
if (serviceTokenData.environment !== secret.environment) { if (!serviceTokenData.permissions.includes(permission)) {
// case: invalid environment passed throw UnauthorizedRequestError({
throw UnauthorizedRequestError({ message: `Failed service token authorization for the given workspace environment action: ${permission}`
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}`,
});
}
}); });
}
}); });
} });
};
+4 -2
View File
@@ -181,14 +181,16 @@ type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
Workspace string `json:"workspace"` Workspace string `json:"workspace"`
Environment string `json:"environment"`
ExpiresAt time.Time `json:"expiresAt"` ExpiresAt time.Time `json:"expiresAt"`
EncryptedKey string `json:"encryptedKey"` EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"` Iv string `json:"iv"`
Tag string `json:"tag"` Tag string `json:"tag"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
SecretPath string `json:"secretPath"` Scopes []struct {
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
} `json:"scopes"`
} }
type GetAccessibleEnvironmentsRequest struct { type GetAccessibleEnvironmentsRequest struct {
+1 -1
View File
@@ -83,7 +83,7 @@ var exportCmd = &cobra.Command{
var output string var output string
if shouldExpandSecrets { if shouldExpandSecrets {
substitutions := util.SubstituteSecrets(secrets) substitutions := util.ExpandSecrets(secrets, infisicalToken)
output, err = formatEnvs(substitutions, format) output, err = formatEnvs(substitutions, format)
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
+1 -1
View File
@@ -100,7 +100,7 @@ var runCmd = &cobra.Command{
} }
if shouldExpandSecrets { if shouldExpandSecrets {
secrets = util.SubstituteSecrets(secrets) secrets = util.ExpandSecrets(secrets, infisicalToken)
} }
secretsByKey := getSecretsByKeys(secrets) secretsByKey := getSecretsByKeys(secrets)
+1 -1
View File
@@ -65,7 +65,7 @@ var secretsCmd = &cobra.Command{
} }
if shouldExpandSecrets { if shouldExpandSecrets {
secrets = util.SubstituteSecrets(secrets) secrets = util.ExpandSecrets(secrets, infisicalToken)
} }
visualize.PrintAllSecretDetails(secrets) visualize.PrintAllSecretDetails(secrets)
+1 -1
View File
@@ -45,5 +45,5 @@ func PrintErrorMessageAndExit(messages ...string) {
} }
func printError(e error) { func printError(e error) {
color.New(color.FgRed).Fprintf(os.Stderr, "Hmm, we ran into an error: %v", e) color.New(color.FgRed).Fprintf(os.Stderr, "Hmm, we ran into an error: %v\n", e)
} }
+108 -21
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path"
"regexp" "regexp"
"strings" "strings"
@@ -16,7 +17,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -34,10 +35,19 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err) return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
} }
// if multiple scopes are there then user needs to specify which environment and secret path
if environment == "" {
if len(serviceTokenDetails.Scopes) != 1 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("you need to provide the --env for multiple environment scoped token")
} else {
environment = serviceTokenDetails.Scopes[0].Environment
}
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: environment,
SecretPath: serviceTokenDetails.SecretPath, SecretPath: secretPath,
}) })
if err != nil { if err != nil {
@@ -189,11 +199,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
} else { } else {
log.Debug().Msg("Trying to fetch secrets using service token") log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken) secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath)
// if serviceTokenDetails.Environment != params.Environment {
// PrintErrorMessageAndExit(fmt.Sprintf("Fetch secrets failed: token allows [%s] environment access, not [%s]. Service tokens are environment-specific; no need for --env flag.", params.Environment, serviceTokenDetails.Environment))
// }
} }
return secretsToReturn, errorToReturn return secretsToReturn, errorToReturn
@@ -279,22 +285,103 @@ func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variable
return "${" + variableWeAreLookingFor + "}" return "${" + variableWeAreLookingFor + "}"
} }
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable { var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
hashMapOfCompleteVariables := make(map[string]string)
hashMapOfSelfRefs := make(map[string]string)
expandedSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range secrets {
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs)
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
Key: secret.Key,
Value: expandedVariable,
Type: secret.Type,
})
func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string {
if v, ok := expandedSecs[key]; ok {
return v
} }
return expandedSecrets interpolatedVal, ok := interpolatedSecs[key]
if !ok {
HandleError(fmt.Errorf("Could not find refered secret - %s", key), "Kindly check whether its provided")
}
refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
for _, val := range refs {
// key: "${something}" val: [${something},something]
interpolatedExp, interpolationKey := val[0], val[1]
ref := strings.Split(interpolationKey, ".")
// ${KEY1} => [key1]
if len(ref) == 1 {
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
continue
}
// cross board reference ${env.folder.key1} => [env folder key1]
if len(ref) > 1 {
secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1]
interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
}
}
expandedSecs[key] = interpolatedVal
return interpolatedVal
}
func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable {
secretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets))
for _, secret := range secrets {
secretMapByName[secret.Key] = secret
}
return secretMapByName
}
func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken string) []models.SingleEnvironmentVariable {
expandedSecs := make(map[string]string)
interpolatedSecs := make(map[string]string)
// map[env.secret-path][keyname]Secret
crossEnvRefSecs := make(map[string]map[string]models.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets
for _, sec := range secrets {
// get all references in a secret
refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1)
// nil means its a secret without reference
if refs == nil {
expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation
} else {
interpolatedSecs[sec.Key] = sec.Value
}
}
for i, sec := range secrets {
// already present pick that up
if expandedVal, ok := expandedSecs[sec.Key]; ok {
secrets[i].Value = expandedVal
continue
}
expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string {
secPaths = append([]string{"/"}, secPaths...)
secPath := path.Join(secPaths...)
secPathDot := strings.Join(secPaths, ".")
uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
// if not in cross reference cache, fetch it from server
refSecs, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: infisicalToken, SecretsPath: secPath})
if err != nil {
HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
}
refSecsByKey := getSecretsByKeys(refSecs)
// save it to avoid calling api again for same environment and folder path
crossEnvRefSecs[uniqKey] = refSecsByKey
return refSecsByKey[secKey].Value
} else {
return crossRefSec[secKey].Value
}
}, sec.Key)
secrets[i].Value = expandedVal
}
return secrets
} }
func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable { func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable {
+2
View File
@@ -37,6 +37,8 @@ For more information on integrations, [refer infisical integration](/integration
You can scope the secrets that can be read and written using an Infisical token by providing the secret path option when creating the token. You can scope the secrets that can be read and written using an Infisical token by providing the secret path option when creating the token.
You can provide the folder path as glob if you want to have access to multiple folders and the tokens do support multi-environment.
![folder scoped service token](../../images/project-folder-token.png) ![folder scoped service token](../../images/project-folder-token.png)
For more information, [refer infisical token section.](./token) For more information, [refer infisical token section.](./token)
@@ -0,0 +1,26 @@
---
title: "Reference Secrets"
description: "How to use reference secrets in Infisical"
---
You can use the interpolation syntax to reference a secret in the same environment, another folder, or another environment
The interpolation syntax is a way of referencing a secret by using a special placeholder. The placeholder is the name of the secret, followed by the environment or folder name, separated by a colon.
For example, to reference a secret named mysecret in the same environment, you would use the placeholder `${mysecret}`.
While for another environment like `test` would be `${test.mysecret}`
Some more examples of referencing are
| Syntax | Environment | Folder | Secret Key |
| --------------------- | ----------- | ------------ | ---------- |
| `${KEY1}` | same env | ssame folder | KEY1 |
| `${dev.KEY2}` | dev | / | KEY2 |
| `${test.frontend.KEY2}` | test | /frontend | KEY2 |
# Permission system for reference
When you use the infisical CLI to log in, the permission system will work the same way as your user permissions.
This means that if you have permission to access other environments, your references to those environments will be resolved.
When using the Infisical CLI with a service token, the service token must have permissions to the referenced environment and folder path.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

After

Width:  |  Height:  |  Size: 70 KiB

+51 -35
View File
@@ -39,9 +39,8 @@ The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/
## Sync Infisical Secrets to your cluster ## Sync Infisical Secrets to your cluster
To retrieve secrets from an Infisical project and save them as native Kubernetes secrets within a specific namespace, utilize the `InfisicalSecret` custom resource definition (CRD). To retrieve secrets from an Infisical project and save them as native Kubernetes secrets within a specific namespace, utilize the `InfisicalSecret` custom resource definition (CRD).
This resource can be created after installing the Infisical operator. For each new managed secret, you will need to create a new InfisicalSecret CRD.
```yaml ```yaml example-infisical-secret-crd.yaml
apiVersion: secrets.infisical.com/v1alpha1 apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret kind: InfisicalSecret
metadata: metadata:
@@ -50,15 +49,18 @@ metadata:
spec: spec:
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used # The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
hostAPI: https://app.infisical.com/api hostAPI: https://app.infisical.com/api
resyncInterval: 60 # <-- the time in seconds between secret re-sync. Faster re-syncs will require higher rate limits resyncInterval:
authentication: authentication:
serviceToken: serviceToken:
serviceTokenSecretReference: serviceTokenSecretReference:
secretName: service-token secretName: service-token
secretNamespace: option secretNamespace: option
secretsScope:
envSlug: dev
secretsPath: "/"
managedSecretReference: managedSecretReference:
secretName: managed-secret # <-- the name of kubernetes secret that will be created secretName: managed-secret # <-- the name of kubernetes secret that will be created
secretNamespace: default # <-- where the kubernetes secret that will be created secretNamespace: default # <-- where the kubernetes secret should be created
``` ```
### InfisicalSecret CRD properties ### InfisicalSecret CRD properties
@@ -86,45 +88,59 @@ Default re-sync interval is every 1 minute.
</Accordion> </Accordion>
<Accordion title="authentication"> <Accordion title="authentication">
The `authentication` property tells the operator where it should look to find credentials needed to fetch secrets from Infisical. This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched. Currently, only [Service Tokens](../../documentation/platform/token) can be used to authenticate with Infisical.
</Accordion>
<Tabs> <Accordion title="authentication.serviceToken.serviceTokenSecretReference">
<Tab title="Service Token"> The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and name space of secret that stores this service token.
Authenticating with service tokens is a great option when you have a small number of services you'd like to fetch secrets for and are looking for the least amount of setup. Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
#### 1. Generate service token
You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. #### 1. Generate service token
#### 2. Create Kubernetes secret containing service token You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings.
Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated. #### 2. Create Kubernetes secret containing service token
To quickly create a Kubernetes secret containing the generated service token, you can run the command below.
``` bash Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated.
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here> To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
```
#### 3. Add reference for the Kubernetes secret containing service token ``` bash
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here>
```
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource. #### 3. Add reference for the Kubernetes secret containing service token
## Example Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
```yaml
apiVersion: secrets.infisical.com/v1alpha1 ## Example
kind: InfisicalSecret ```yaml
metadata: apiVersion: secrets.infisical.com/v1alpha1
name: infisicalsecret-sample-crd kind: InfisicalSecret
spec: metadata:
authentication: name: infisicalsecret-sample-crd
serviceToken: spec:
serviceTokenSecretReference: authentication:
secretName: service-token # <-- name of the Kubernetes secret that stores our service token serviceToken:
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token serviceTokenSecretReference:
... secretName: service-token # <-- name of the Kubernetes secret that stores our service token
``` secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
</Tab> ...
</Tabs> ```
</Accordion>
<Accordion title="authentication.serviceToken.secretsScope">
This block defines the scope of what secrets should be fetched. This is needed as your service token can have access to multiple folders and environments.
A scope is defined by `envSlug` and `secretsPath`.
#### envSlug
This refers to the short hand name of an environment. For example for the `development` environment the environment slug is `dev`. You can locate the slug of your environment by heading to your project settings in the Infisical dashboard.
#### secretsPath
secretsPath is the path to the secret in the given environment. For example a path of `/` would refer to the root of the environment whereas `/folder1` would refer to the secrets in folder1 from the root.
Both fields are required.
</Accordion> </Accordion>
<Accordion title="managedSecretReference"> <Accordion title="managedSecretReference">
+1
View File
@@ -110,6 +110,7 @@
"documentation/platform/organization", "documentation/platform/organization",
"documentation/platform/project", "documentation/platform/project",
"documentation/platform/folder", "documentation/platform/folder",
"documentation/platform/secret-reference",
"documentation/platform/pit-recovery", "documentation/platform/pit-recovery",
"documentation/platform/secret-versioning", "documentation/platform/secret-versioning",
"documentation/platform/audit-logs", "documentation/platform/audit-logs",
@@ -1,9 +1,13 @@
export type ServiceTokenScope = {
environment: string;
secretPath: string;
};
export type ServiceToken = { export type ServiceToken = {
_id: string; _id: string;
name: string; name: string;
workspace: string; workspace: string;
environment: string; scopes: ServiceTokenScope[];
secretPath: string;
user: string; user: string;
expiresAt: string; expiresAt: string;
createdAt: string; createdAt: string;
@@ -14,9 +18,8 @@ export type ServiceToken = {
export type CreateServiceTokenDTO = { export type CreateServiceTokenDTO = {
name: string; name: string;
workspaceId: string; workspaceId: string;
environment: string; scopes: ServiceTokenScope[];
expiresIn: number; expiresIn: number;
secretPath: string;
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
@@ -1,9 +1,9 @@
import crypto from "crypto"; import crypto from "crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
@@ -27,10 +27,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
useCreateServiceToken,
useGetUserWsKey
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const apiTokenExpiry = [ const apiTokenExpiry = [
@@ -44,8 +41,23 @@ const apiTokenExpiry = [
const schema = yup.object({ const schema = yup.object({
name: yup.string().max(100).required().label("Service Token Name"), name: yup.string().max(100).required().label("Service Token Name"),
environment: yup.string().max(50).required().label("Environment"), scopes: yup
secretPath: yup.string().required().default("/").label("Secret Path"), .array(
yup.object({
environment: yup.string().max(50).required().label("Environment"),
secretPath: yup
.string()
.required()
.default("/")
.label("Secret Path")
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
)
})
)
.min(1)
.required()
.label("Scope"),
expiresIn: yup.string().optional().label("Service Token Expiration"), expiresIn: yup.string().optional().label("Service Token Expiration"),
permissions: yup permissions: yup
.object() .object()
@@ -60,284 +72,301 @@ const schema = yup.object({
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["createAPIToken"]>; popUp: UsePopUpState<["createAPIToken"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void;
}; };
export const AddServiceTokenModal = ({ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
popUp, const { t } = useTranslation();
handlePopUpToggle const { createNotification } = useNotificationContext();
}: Props) => { const { currentWorkspace } = useWorkspace();
const { t } = useTranslation(); const {
const { createNotification } = useNotificationContext(); control,
const { currentWorkspace } = useWorkspace(); reset,
const { handleSubmit,
control, formState: { isSubmitting }
reset, } = useForm<FormData>({
handleSubmit, resolver: yupResolver(schema),
formState: { isSubmitting } defaultValues: {
} = useForm<FormData>({ scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }]
resolver: yupResolver(schema) }
}); });
const [newToken, setToken] = useState(""); const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); const [newToken, setToken] = useState("");
const createServiceToken = useCreateServiceToken(); const [isTokenCopied, setIsTokenCopied] = useToggle(false);
const hasServiceToken = Boolean(newToken);
useEffect(() => { const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
let timer: NodeJS.Timeout; const createServiceToken = useCreateServiceToken();
if (isTokenCopied) { const hasServiceToken = Boolean(newToken);
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
}
return () => clearTimeout(timer); useEffect(() => {
}, [isTokenCopied]); let timer: NodeJS.Timeout;
if (isTokenCopied) {
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
}
const copyTokenToClipboard = () => { return () => clearTimeout(timer);
navigator.clipboard.writeText(newToken); }, [isTokenCopied]);
setIsTokenCopied.on();
};
const onFormSubmit = async ({ const copyTokenToClipboard = () => {
navigator.clipboard.writeText(newToken);
setIsTokenCopied.on();
};
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
try {
if (!currentWorkspace?._id) return;
if (!latestFileKey) return;
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: key,
key: randomBytes
});
const { serviceToken } = await createServiceToken.mutateAsync({
encryptedKey: ciphertext,
iv,
tag,
scopes,
expiresIn: Number(expiresIn),
name, name,
environment, workspaceId: currentWorkspace._id,
secretPath, randomBytes,
expiresIn, permissions: Object.entries(permissions)
permissions .filter(([, permissionsValue]) => permissionsValue)
}: FormData) => { .map(([permissionsKey]) => permissionsKey)
try { });
if (!currentWorkspace?._id) return;
if (!latestFileKey) return;
const key = decryptAssymmetric({ setToken(serviceToken);
ciphertext: latestFileKey.encryptedKey, createNotification({
nonce: latestFileKey.nonce, text: "Successfully created a service token",
publicKey: latestFileKey.sender.publicKey, type: "success"
privateKey: localStorage.getItem("PRIVATE_KEY") as string });
}); } catch (err) {
console.error(err);
createNotification({
text: "Failed to create a service token",
type: "error"
});
}
};
const randomBytes = crypto.randomBytes(16).toString("hex"); return (
<Modal
const { ciphertext, iv, tag } = encryptSymmetric({ isOpen={popUp?.createAPIToken?.isOpen}
plaintext: key, onOpenChange={(open) => {
key: randomBytes handlePopUpToggle("createAPIToken", open);
}); reset();
setToken("");
const { serviceToken } = await createServiceToken.mutateAsync({ }}
encryptedKey: ciphertext, >
iv, <ModalContent
tag, title={
environment, t("section.token.add-dialog.title", {
secretPath, target: currentWorkspace?.name
expiresIn: Number(expiresIn), }) as string
name,
workspaceId: currentWorkspace._id,
randomBytes,
permissions: Object.entries(permissions)
.filter(([, permissionsValue]) => permissionsValue)
.map(([permissionsKey]) => permissionsKey)
});
setToken(serviceToken);
createNotification({
text: "Successfully created a service token",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create a service token",
type: "error"
});
} }
}; subTitle={t("section.token.add-dialog.description") as string}
>
return ( {!hasServiceToken ? (
<Modal <form onSubmit={handleSubmit(onFormSubmit)}>
isOpen={popUp?.createAPIToken?.isOpen} <Controller
onOpenChange={(open) => { control={control}
handlePopUpToggle("createAPIToken", open); name="name"
reset(); defaultValue=""
setToken(""); render={({ field, fieldState: { error } }) => (
}} <FormControl
> label={t("section.token.add-dialog.name")}
<ModalContent isError={Boolean(error)}
title={ errorText={error?.message}
t("section.token.add-dialog.title", { >
target: currentWorkspace?.name <Input {...field} placeholder="Type your token name" />
}) as string </FormControl>
}
subTitle={t("section.token.add-dialog.description") as string}
>
{!hasServiceToken ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label={t("section.token.add-dialog.name")}
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your token name" />
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
defaultValue={currentWorkspace?.environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{currentWorkspace?.environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
isError={Boolean(error)}
helperText="Tokens can be scoped to a folder path. Default path is /"
errorText={error?.message}
>
<Input {...field} placeholder="Provide a path, default is /" />
</FormControl>
)}
/>
<Controller
control={control}
name="expiresIn"
defaultValue={String(apiTokenExpiry?.[0]?.value)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Expiration"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{apiTokenExpiry.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="permissions"
defaultValue={{
read: true,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [
{
label: "Read (default)",
value: "read"
},
{
label: "Write (optional)",
value: "write"
}
];
return (
<FormControl
label="Permissions"
errorText={error?.message}
isError={Boolean(error)}
>
<>
{options.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={value[optionValue]}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
isDisabled={optionValue === "read"}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</>
</FormControl>
);
}}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{newToken}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
)} )}
</ModalContent> />
</Modal> {tokenScopes.map(({ id }, index) => (
); <div className="mb-3 flex items-end space-x-2" key={id}>
} <Controller
control={control}
name={`scopes.${index}.environment`}
defaultValue={currentWorkspace?.environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="mb-0"
label={index === 0 ? "Environment" : undefined}
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{currentWorkspace?.environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name={`scopes.${index}.secretPath`}
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Secrets Path" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
</FormControl>
)}
/>
<IconButton
className="p-3"
ariaLabel="remove"
colorSchema="danger"
onClick={() => remove(index)}
>
<FontAwesomeIcon icon={faTrashCan} size="sm" />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
append({
environment: currentWorkspace?.environments?.[0]?.slug || "",
secretPath: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Scope
</Button>
</div>
<Controller
control={control}
name="expiresIn"
defaultValue={String(apiTokenExpiry?.[0]?.value)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Expiration" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{apiTokenExpiry.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="permissions"
defaultValue={{
read: true,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [
{
label: "Read (default)",
value: "read"
},
{
label: "Write (optional)",
value: "write"
}
];
return (
<FormControl
label="Permissions"
errorText={error?.message}
isError={Boolean(error)}
>
<>
{options.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={value[optionValue]}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
isDisabled={optionValue === "read"}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</>
</FormControl>
);
}}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{newToken}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
)}
</ModalContent>
</Modal>
);
};
@@ -3,14 +3,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { Button, DeleteActionModal } from "@app/components/v2";
Button,
DeleteActionModal,
} from "@app/components/v2";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import { useDeleteServiceToken } from "@app/hooks/api";
useDeleteServiceToken
} from "@app/hooks/api";
import { AddServiceTokenModal } from "./AddServiceTokenModal"; import { AddServiceTokenModal } from "./AddServiceTokenModal";
import { ServiceTokenTable } from "./ServiceTokenTable"; import { ServiceTokenTable } from "./ServiceTokenTable";
@@ -29,7 +24,9 @@ export const ServiceTokenSection = () => {
const onDeleteApproved = async () => { const onDeleteApproved = async () => {
try { try {
deleteServiceToken.mutateAsync((popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id); deleteServiceToken.mutateAsync(
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
);
createNotification({ createNotification({
text: "Successfully deleted service token", text: "Successfully deleted service token",
type: "success" type: "success"
@@ -46,32 +43,29 @@ export const ServiceTokenSection = () => {
}; };
return ( return (
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600"> <div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8"> <div className="mb-2 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">{t("section.token.service-tokens")}</p> <p className="text-xl font-semibold text-mineshaft-100">
<Button {t("section.token.service-tokens")}
colorSchema="secondary" </p>
leftIcon={<FontAwesomeIcon icon={faPlus} />} <Button
onClick={() => { colorSchema="secondary"
handlePopUpOpen("createAPIToken"); leftIcon={<FontAwesomeIcon icon={faPlus} />}
}} onClick={() => {
> handlePopUpOpen("createAPIToken");
Create token }}
</Button> >
Create token
</Button>
</div> </div>
<p className="text-gray-400 mb-8">{t("section.token.service-tokens-description")}</p> <p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
<ServiceTokenTable <ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
handlePopUpOpen={handlePopUpOpen} <AddServiceTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
/>
<AddServiceTokenModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteAPITokenConfirmation.isOpen} isOpen={popUp.deleteAPITokenConfirmation.isOpen}
title={ title={`Delete ${
`Delete ${(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "} service token?` (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "
} } service token?`}
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name} deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")} onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
@@ -1,4 +1,4 @@
import { faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons"; import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -18,71 +18,82 @@ import { useGetUserWsServiceTokens } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>, popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>,
{ {
name, name,
id id
}: { }: {
name: string; name: string;
id: string; id: string;
} }
) => void; ) => void;
}; };
export const ServiceTokenTable = ({ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
handlePopUpOpen const { currentWorkspace } = useWorkspace();
}: Props) => { const { data, isLoading } = useGetUserWsServiceTokens({
const { currentWorkspace } = useWorkspace(); workspaceID: currentWorkspace?._id || ""
const { data, isLoading } = useGetUserWsServiceTokens({ });
workspaceID: currentWorkspace?._id || ""
});
return ( return (
<TableContainer> <TableContainer>
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th>Token Name</Th> <Th>Token Name</Th>
<Th>Environment</Th> <Th>Envrionment - Secret Path</Th>
<Th>Secret Path</Th> <Th>Valid Until</Th>
<Th>Valid Until</Th> <Th aria-label="button" />
<Th aria-label="button" /> </Tr>
</Tr> </THead>
</THead> <TBody>
<TBody> {isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />} {!isLoading &&
{!isLoading && data && data.map((row) => ( data &&
<Tr key={row._id}> data.map((row) => (
<Td>{row.name}</Td> <Tr key={row._id}>
<Td>{row.environment}</Td> <Td>{row.name}</Td>
<Td>{row.secretPath}</Td> <Td>
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td> <div className="mb-2 flex flex-col flex-wrap space-y-1">
<Td className="flex items-center justify-end"> {row?.scopes.map(({ secretPath, environment }) => (
<IconButton <div
onClick={() => key={`${row._id}-${environment}-${secretPath}`}
handlePopUpOpen("deleteAPITokenConfirmation", { className="inline-flex items-center space-x-1 rounded-md border border-mineshaft-600 p-1 px-2"
name: row.name, >
id: row._id <div className="mr-2 border-r border-mineshaft-600 pr-2">{environment}</div>
}) <FontAwesomeIcon icon={faFolder} size="sm" />
} <span className="pl-2">{secretPath}</span>
colorSchema="danger" </div>
ariaLabel="delete"
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
</Td>
</Tr>
))} ))}
{!isLoading && data && data?.length === 0 && ( </div>
<Tr> </Td>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400"> <Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
<EmptyState title="No service tokens found" icon={faKey} /> <Td>
</Td> <IconButton
</Tr> onClick={() =>
)} handlePopUpOpen("deleteAPITokenConfirmation", {
</TBody> name: row.name,
</Table> id: row._id
</TableContainer> })
); }
} colorSchema="danger"
ariaLabel="delete"
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
</Td>
</Tr>
))}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="No service tokens found" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
+2 -2
View File
@@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.6 version: 0.2.0
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "0.1.8" appVersion: "0.2.0"
@@ -63,6 +63,16 @@ spec:
type: object type: object
serviceToken: serviceToken:
properties: properties:
secretsScope:
properties:
envSlug:
type: string
secretsPath:
type: string
required:
- envSlug
- secretsPath
type: object
serviceTokenSecretReference: serviceTokenSecretReference:
properties: properties:
secretName: secretName:
@@ -77,6 +87,7 @@ spec:
- secretNamespace - secretNamespace
type: object type: object
required: required:
- secretsScope
- serviceTokenSecretReference - serviceTokenSecretReference
type: object type: object
type: object type: object
@@ -4,8 +4,19 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
type Authentication struct {
// +kubebuilder:validation:Optional
ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
// +kubebuilder:validation:Optional
ServiceToken ServiceTokenDetails `json:"serviceToken"`
}
type ServiceTokenDetails struct { type ServiceTokenDetails struct {
// +kubebuilder:validation:Required
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"` ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
// +kubebuilder:validation:Required
SecretsScope SecretScopeInWorkspace `json:"secretsScope"`
} }
type ServiceAccountDetails struct { type ServiceAccountDetails struct {
@@ -14,11 +25,12 @@ type ServiceAccountDetails struct {
EnvironmentName string `json:"environmentName"` EnvironmentName string `json:"environmentName"`
} }
type Authentication struct { type SecretScopeInWorkspace struct {
// +kubebuilder:validation:Optional // +kubebuilder:validation:Required
ServiceAccount ServiceAccountDetails `json:"serviceAccount"` SecretsPath string `json:"secretsPath"`
// +kubebuilder:validation:Optional
ServiceToken ServiceTokenDetails `json:"serviceToken"` // +kubebuilder:validation:Required
EnvSlug string `json:"envSlug"`
} }
type KubeSecretReference struct { type KubeSecretReference struct {
@@ -157,6 +157,21 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretScopeInWorkspace.
func (in *SecretScopeInWorkspace) DeepCopy() *SecretScopeInWorkspace {
if in == nil {
return nil
}
out := new(SecretScopeInWorkspace)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) { func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) {
*out = *in *out = *in
@@ -177,6 +192,7 @@ func (in *ServiceAccountDetails) DeepCopy() *ServiceAccountDetails {
func (in *ServiceTokenDetails) DeepCopyInto(out *ServiceTokenDetails) { func (in *ServiceTokenDetails) DeepCopyInto(out *ServiceTokenDetails) {
*out = *in *out = *in
out.ServiceTokenSecretReference = in.ServiceTokenSecretReference out.ServiceTokenSecretReference = in.ServiceTokenSecretReference
out.SecretsScope = in.SecretsScope
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTokenDetails. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTokenDetails.
@@ -63,6 +63,16 @@ spec:
type: object type: object
serviceToken: serviceToken:
properties: properties:
secretsScope:
properties:
envSlug:
type: string
secretsPath:
type: string
required:
- envSlug
- secretsPath
type: object
serviceTokenSecretReference: serviceTokenSecretReference:
properties: properties:
secretName: secretName:
@@ -77,6 +87,7 @@ spec:
- secretNamespace - secretNamespace
type: object type: object
required: required:
- secretsScope
- serviceTokenSecretReference - serviceTokenSecretReference
type: object type: object
type: object type: object
+9 -6
View File
@@ -3,8 +3,8 @@ kind: InfisicalSecret
metadata: metadata:
name: infisicalsecret-sample name: infisicalsecret-sample
spec: spec:
hostAPI: http://localhost:7070/api hostAPI: http://localhost:8764/api
resyncInterval: 60 resyncInterval: 10
authentication: authentication:
serviceAccount: serviceAccount:
serviceAccountSecretReference: serviceAccountSecretReference:
@@ -16,10 +16,13 @@ spec:
serviceTokenSecretReference: serviceTokenSecretReference:
secretName: service-token secretName: service-token
secretNamespace: default secretNamespace: default
secretsScope:
envSlug: dev
secretsPath: "/"
managedSecretReference: managedSecretReference:
secretName: managed-secret secretName: managed-secret
secretNamespace: default secretNamespace: default
# To be depreciated soon # # To be depreciated soon
tokenSecretReference: # tokenSecretReference:
secretName: service-token # secretName: service-token
secretNamespace: default # secretNamespace: default
@@ -219,7 +219,10 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account") fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
} else if infisicalToken != "" { } else if infisicalToken != "" {
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag) envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath)
if err != nil { if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
} }
@@ -70,6 +70,16 @@ spec:
type: object type: object
serviceToken: serviceToken:
properties: properties:
secretsScope:
properties:
envSlug:
type: string
secretsPath:
type: string
required:
- envSlug
- secretsPath
type: object
serviceTokenSecretReference: serviceTokenSecretReference:
properties: properties:
secretName: secretName:
@@ -83,6 +93,7 @@ spec:
- secretNamespace - secretNamespace
type: object type: object
required: required:
- secretsScope
- serviceTokenSecretReference - serviceTokenSecretReference
type: object type: object
type: object type: object
+110 -4
View File
@@ -3,6 +3,8 @@ package util
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"path"
"regexp"
"strings" "strings"
"github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/api"
@@ -48,7 +50,7 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
return serviceTokenDetails, nil return serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -68,9 +70,9 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: envSlug,
ETag: etag, ETag: etag,
SecretPath: serviceTokenDetails.SecretPath, SecretPath: secretPath,
}) })
if err != nil { if err != nil {
@@ -92,7 +94,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
return plainTextSecrets, encryptedSecretsResponse, nil // expand secrets that are referenced
expandedSecrets := ExpandSecrets(plainTextSecrets, fullServiceToken)
return expandedSecrets, encryptedSecretsResponse, nil
} }
// Fetches plaintext secrets from an API endpoint using a service account. // Fetches plaintext secrets from an API endpoint using a service account.
@@ -252,3 +257,104 @@ func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSe
return plainTextSecrets, nil return plainTextSecrets, nil
} }
var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string {
if v, ok := expandedSecs[key]; ok {
return v
}
interpolatedVal, ok := interpolatedSecs[key]
if !ok {
return ""
// panic(fmt.Errorf("Could not find referred secret with key name %s", key), "Please check it refers a")
}
refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
for _, val := range refs {
// key: "${something}" val: [${something},something]
interpolatedExp, interpolationKey := val[0], val[1]
ref := strings.Split(interpolationKey, ".")
// ${KEY1} => [key1]
if len(ref) == 1 {
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
continue
}
// cross board reference ${env.folder.key1} => [env folder key1]
if len(ref) > 1 {
secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1]
interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
}
}
expandedSecs[key] = interpolatedVal
return interpolatedVal
}
func ExpandSecrets(secrets []model.SingleEnvironmentVariable, infisicalToken string) []model.SingleEnvironmentVariable {
expandedSecs := make(map[string]string)
interpolatedSecs := make(map[string]string)
// map[env.secret-path][keyname]Secret
crossEnvRefSecs := make(map[string]map[string]model.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets
for _, sec := range secrets {
// get all references in a secret
refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1)
// nil means its a secret without reference
if refs == nil {
expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation
} else {
interpolatedSecs[sec.Key] = sec.Value
}
}
for i, sec := range secrets {
// already present pick that up
if expandedVal, ok := expandedSecs[sec.Key]; ok {
secrets[i].Value = expandedVal
continue
}
expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string {
secPaths = append([]string{"/"}, secPaths...)
secPath := path.Join(secPaths...)
secPathDot := strings.Join(secPaths, ".")
uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
// if not in cross reference cache, fetch it from server
refSecs, _, err := GetPlainTextSecretsViaServiceToken(infisicalToken, "", env, secPath)
if err != nil {
fmt.Println("HELLO===>", "MOO", err)
// HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
}
refSecsByKey := getSecretsByKeys(refSecs)
// save it to avoid calling api again for same environment and folder path
crossEnvRefSecs[uniqKey] = refSecsByKey
return refSecsByKey[secKey].Value
} else {
return crossRefSec[secKey].Value
}
}, sec.Key)
secrets[i].Value = expandedVal
}
return secrets
}
func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]model.SingleEnvironmentVariable {
secretMapByName := make(map[string]model.SingleEnvironmentVariable, len(secrets))
for _, secret := range secrets {
secretMapByName[secret.Key] = secret
}
return secretMapByName
}