mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
135 Commits
v0.85.0-po
...
doc/add-gr
Author | SHA1 | Date | |
---|---|---|---|
3e3f42a8f7 | |||
da86338bfe | |||
3a9a6767a0 | |||
fe8a1e6ce6 | |||
55aa3f7b58 | |||
59f3581370 | |||
ccae63936c | |||
50b51f1810 | |||
fc39b3b0dd | |||
5964976e47 | |||
677a87150b | |||
2469c8d0c6 | |||
dafb89d1dd | |||
8da01445e5 | |||
6b2273d314 | |||
b886e66ee9 | |||
3afcb19727 | |||
06d2480f30 | |||
fd7d8ddf2d | |||
1dc0f4e5b8 | |||
fa64a88c24 | |||
385ec05e57 | |||
3a38e1e413 | |||
7f04e9e97d | |||
839f0c7e1c | |||
2352e29902 | |||
fcbc7fcece | |||
c2252c65a4 | |||
e150673de4 | |||
4f5c49a529 | |||
7107089ad3 | |||
967818f57d | |||
02111c2dc2 | |||
ebea74b607 | |||
5bbe5421bf | |||
279289989f | |||
bb4a16cf7c | |||
309db49f1b | |||
62a582ef17 | |||
d6b389760d | |||
bd4deb02b0 | |||
449e7672f9 | |||
31ff6d3c17 | |||
cfcc32271f | |||
e2ea84f28a | |||
6885ef2e54 | |||
8fa9f476e3 | |||
1cf8d1e3fa | |||
9f61177b62 | |||
59b8e83476 | |||
eee4d00a08 | |||
51c0598b50 | |||
69311f058b | |||
0f70c3ea9a | |||
b5660c87a0 | |||
2a686e65cd | |||
2bb0386220 | |||
526605a0bb | |||
5b9903a226 | |||
3fc60bf596 | |||
7815d6538f | |||
4c4d525655 | |||
e44213a8a9 | |||
e87656631c | |||
e102ccf9f0 | |||
63af75a330 | |||
8a10af9b62 | |||
18308950d1 | |||
86a9676a9c | |||
aa12a71ff3 | |||
aee46d1902 | |||
279a1791f6 | |||
8d71b295ea | |||
f72cedae10 | |||
864cf23416 | |||
10574bfe26 | |||
02085ce902 | |||
4eeea0b27c | |||
93b7f56337 | |||
12ecefa832 | |||
dd9a00679d | |||
081502848d | |||
0fa9fa20bc | |||
0a1f25a659 | |||
bc74c44f97 | |||
c50e325f53 | |||
0225e6fabb | |||
3caa46ade8 | |||
998bbe92f7 | |||
009be0ded8 | |||
c9f6207e32 | |||
36adc5e00e | |||
cb24b2aac8 | |||
1e0eb26dce | |||
f8161c8c72 | |||
862e2e9d65 | |||
0e734bd638 | |||
a35054f6ba | |||
e0ace85d6e | |||
7867587884 | |||
0564d06923 | |||
8ace72d134 | |||
491331e9e3 | |||
4a324eafd8 | |||
173cf0238d | |||
fd792e7e1d | |||
d0656358a2 | |||
040fa511f6 | |||
75099f159f | |||
e4a83ad2e2 | |||
760f9d487c | |||
a02e73e2a4 | |||
d4c95ab1a7 | |||
fbebeaf38f | |||
97245c740e | |||
03c4c2056a | |||
cee982754b | |||
a6497b844a | |||
788dcf2c73 | |||
6d9f80805e | |||
7f055450df | |||
9234213c62 | |||
5a40b5a1cf | |||
19e4a6de4d | |||
0daca059c7 | |||
e7278c4cd9 | |||
3e79dbb3f5 | |||
0fd193f8e0 | |||
342c713805 | |||
9b2565e387 | |||
1c5a8cabe9 | |||
613b97c93d | |||
335f3f7d37 | |||
b3f0d36ddc | |||
dbb8617180 |
@ -1,6 +1,7 @@
|
|||||||
import { seedData1 } from "@app/db/seed-data";
|
import { seedData1 } from "@app/db/seed-data";
|
||||||
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
|
|
||||||
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => {
|
const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/v1/secret-approvals`,
|
url: `/api/v1/secret-approvals`,
|
||||||
@ -26,7 +27,7 @@ describe("Secret approval policy router", async () => {
|
|||||||
const policy = await createPolicy({
|
const policy = await createPolicy({
|
||||||
secretPath: "/",
|
secretPath: "/",
|
||||||
approvals: 1,
|
approvals: 1,
|
||||||
approvers: [seedData1.id],
|
approvers: [{id:seedData1.id, type: ApproverType.User}],
|
||||||
name: "test-policy"
|
name: "test-policy"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
417
backend/package-lock.json
generated
417
backend/package-lock.json
generated
@ -81,7 +81,7 @@
|
|||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"pkijs": "^3.2.4",
|
"pkijs": "^3.2.4",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.3.8",
|
||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
@ -8018,6 +8018,7 @@
|
|||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-types": "~2.1.34",
|
"mime-types": "~2.1.34",
|
||||||
"negotiator": "0.6.3"
|
"negotiator": "0.6.3"
|
||||||
@ -8336,7 +8337,8 @@
|
|||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/array-includes": {
|
"node_modules/array-includes": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.7",
|
||||||
@ -8814,9 +8816,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"content-type": "~1.0.5",
|
"content-type": "~1.0.5",
|
||||||
@ -8826,7 +8829,7 @@
|
|||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"raw-body": "2.5.2",
|
"raw-body": "2.5.2",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
@ -8840,6 +8843,7 @@
|
|||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
@ -8848,6 +8852,7 @@
|
|||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
},
|
},
|
||||||
@ -8858,7 +8863,8 @@
|
|||||||
"node_modules/body-parser/node_modules/ms": {
|
"node_modules/body-parser/node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/bottleneck": {
|
"node_modules/bottleneck": {
|
||||||
"version": "2.19.5",
|
"version": "2.19.5",
|
||||||
@ -9006,6 +9012,7 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
@ -9028,13 +9035,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||||
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
|
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"get-intrinsic": "^1.2.1",
|
"get-intrinsic": "^1.2.4",
|
||||||
"set-function-length": "^1.1.1"
|
"set-function-length": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@ -9379,6 +9392,7 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -9543,16 +9557,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/define-data-property": {
|
"node_modules/define-data-property": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-intrinsic": "^1.2.1",
|
"es-define-property": "^1.0.0",
|
||||||
"gopd": "^1.0.1",
|
"es-errors": "^1.3.0",
|
||||||
"has-property-descriptors": "^1.0.0"
|
"gopd": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/define-lazy-prop": {
|
"node_modules/define-lazy-prop": {
|
||||||
@ -9618,6 +9636,7 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8",
|
"node": ">= 0.8",
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
@ -9724,7 +9743,8 @@
|
|||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.816",
|
"version": "1.4.816",
|
||||||
@ -9738,9 +9758,10 @@
|
|||||||
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
|
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
|
||||||
},
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
@ -9827,6 +9848,27 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-intrinsic": "^1.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-set-tostringtag": {
|
"node_modules/es-set-tostringtag": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
|
||||||
@ -10452,6 +10494,7 @@
|
|||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -10495,36 +10538,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.19.2",
|
"version": "4.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.6.0",
|
"cookie": "0.6.0",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "1.2.0",
|
"finalhandler": "1.3.1",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"merge-descriptors": "1.0.1",
|
"merge-descriptors": "1.0.3",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.7",
|
"path-to-regexp": "0.1.10",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.11.0",
|
"qs": "6.13.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.18.0",
|
"send": "0.19.0",
|
||||||
"serve-static": "1.15.0",
|
"serve-static": "1.16.2",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "2.0.1",
|
"statuses": "2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
@ -10588,6 +10632,7 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -10595,12 +10640,14 @@
|
|||||||
"node_modules/express/node_modules/cookie-signature": {
|
"node_modules/express/node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/express/node_modules/debug": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
@ -10608,7 +10655,8 @@
|
|||||||
"node_modules/express/node_modules/ms": {
|
"node_modules/express/node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/extend": {
|
"node_modules/extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
@ -10815,12 +10863,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
@ -10835,6 +10884,7 @@
|
|||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
@ -10842,7 +10892,8 @@
|
|||||||
"node_modules/finalhandler/node_modules/ms": {
|
"node_modules/finalhandler/node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/find-my-way": {
|
"node_modules/find-my-way": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@ -11008,6 +11059,7 @@
|
|||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -11365,15 +11417,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||||
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
|
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"has-proto": "^1.0.1",
|
"has-proto": "^1.0.1",
|
||||||
"has-symbols": "^1.0.3",
|
"has-symbols": "^1.0.3",
|
||||||
"hasown": "^2.0.0"
|
"hasown": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@ -11719,11 +11776,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-property-descriptors": {
|
"node_modules/has-property-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-intrinsic": "^1.2.2"
|
"es-define-property": "^1.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@ -13276,6 +13334,7 @@
|
|||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -13286,9 +13345,13 @@
|
|||||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
||||||
},
|
},
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -13309,6 +13372,7 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -13748,6 +13812,7 @@
|
|||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -14099,6 +14164,7 @@
|
|||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ee-first": "1.1.1"
|
"ee-first": "1.1.1"
|
||||||
},
|
},
|
||||||
@ -14511,9 +14577,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@ -14716,20 +14783,78 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pino-http": {
|
"node_modules/pino-http": {
|
||||||
"version": "8.6.1",
|
"version": "10.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.3.0.tgz",
|
||||||
"integrity": "sha512-J0hiJgUExtBXP2BjrK4VB305tHXS31sCmWJ9XJo2wPkLHa1NFPuW4V9wjG27PAc2fmBCigiNhQKpvrx+kntBPA==",
|
"integrity": "sha512-kaHQqt1i5S9LXWmyuw6aPPqYW/TjoDPizPs4PnDW4hSpajz2Uo/oisNliLf7We1xzpiLacdntmw8yaZiEkppQQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-caller-file": "^2.0.5",
|
"get-caller-file": "^2.0.5",
|
||||||
"pino": "^8.17.1",
|
"pino": "^9.0.0",
|
||||||
"pino-std-serializers": "^6.2.2",
|
"pino-std-serializers": "^7.0.0",
|
||||||
"process-warning": "^3.0.0"
|
"process-warning": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pino-http/node_modules/pino": {
|
||||||
|
"version": "9.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz",
|
||||||
|
"integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0",
|
||||||
|
"fast-redact": "^3.1.1",
|
||||||
|
"on-exit-leak-free": "^2.1.0",
|
||||||
|
"pino-abstract-transport": "^1.2.0",
|
||||||
|
"pino-std-serializers": "^7.0.0",
|
||||||
|
"process-warning": "^4.0.0",
|
||||||
|
"quick-format-unescaped": "^4.0.3",
|
||||||
|
"real-require": "^0.2.0",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"sonic-boom": "^4.0.1",
|
||||||
|
"thread-stream": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pino": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-http/node_modules/pino-abstract-transport": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^4.0.0",
|
||||||
|
"split2": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-http/node_modules/pino-std-serializers": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pino-http/node_modules/process-warning": {
|
"node_modules/pino-http/node_modules/process-warning": {
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz",
|
||||||
"integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="
|
"integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pino-http/node_modules/sonic-boom": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-http/node_modules/thread-stream": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"real-require": "^0.2.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pino-pretty": {
|
"node_modules/pino-pretty": {
|
||||||
"version": "10.2.3",
|
"version": "10.2.3",
|
||||||
@ -15096,9 +15221,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/probot": {
|
"node_modules/probot": {
|
||||||
"version": "13.0.0",
|
"version": "13.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/probot/-/probot-13.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/probot/-/probot-13.3.8.tgz",
|
||||||
"integrity": "sha512-3ht9kAJ+ISjLyWLLCKVdrLE5xs/x+zUx07J5kYTxAyIxUvwF6Acr8xT5fiNihbBHAsEl4+A4CMYZQvZ5hx5bgw==",
|
"integrity": "sha512-xc+KBC0mp1JKFMsPbMyj1SpmN0B7Q8uFO7ze4PBbNv74q8AyPGqYL3TmkZSOmcOjFTeFrZTnMYEoXi+z1anyLA==",
|
||||||
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/core": "^5.0.2",
|
"@octokit/core": "^5.0.2",
|
||||||
"@octokit/plugin-enterprise-compatibility": "^4.0.1",
|
"@octokit/plugin-enterprise-compatibility": "^4.0.1",
|
||||||
@ -15113,19 +15239,18 @@
|
|||||||
"@probot/octokit-plugin-config": "^2.0.1",
|
"@probot/octokit-plugin-config": "^2.0.1",
|
||||||
"@probot/pino": "^2.3.5",
|
"@probot/pino": "^2.3.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"commander": "^11.1.0",
|
"bottleneck": "^2.19.5",
|
||||||
|
"commander": "^12.0.0",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eventsource": "^2.0.2",
|
"express": "^4.21.0",
|
||||||
"express": "^4.18.2",
|
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lru-cache": "^10.0.3",
|
"lru-cache": "^10.0.3",
|
||||||
"octokit-auth-probot": "^2.0.0",
|
"octokit-auth-probot": "^2.0.0",
|
||||||
"pino": "^8.16.1",
|
"pino": "^9.0.0",
|
||||||
"pino-http": "^8.5.1",
|
"pino-http": "^10.0.0",
|
||||||
"pkg-conf": "^3.1.0",
|
"pkg-conf": "^3.1.0",
|
||||||
"resolve": "^1.22.8",
|
|
||||||
"update-dotenv": "^1.1.1"
|
"update-dotenv": "^1.1.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -15152,11 +15277,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/probot/node_modules/commander": {
|
"node_modules/probot/node_modules/commander": {
|
||||||
"version": "11.1.0",
|
"version": "12.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/probot/node_modules/lru-cache": {
|
"node_modules/probot/node_modules/lru-cache": {
|
||||||
@ -15167,6 +15293,68 @@
|
|||||||
"node": "14 || >=16.14"
|
"node": "14 || >=16.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/probot/node_modules/pino": {
|
||||||
|
"version": "9.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz",
|
||||||
|
"integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0",
|
||||||
|
"fast-redact": "^3.1.1",
|
||||||
|
"on-exit-leak-free": "^2.1.0",
|
||||||
|
"pino-abstract-transport": "^1.2.0",
|
||||||
|
"pino-std-serializers": "^7.0.0",
|
||||||
|
"process-warning": "^4.0.0",
|
||||||
|
"quick-format-unescaped": "^4.0.3",
|
||||||
|
"real-require": "^0.2.0",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"sonic-boom": "^4.0.1",
|
||||||
|
"thread-stream": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pino": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/probot/node_modules/pino-abstract-transport": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^4.0.0",
|
||||||
|
"split2": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/probot/node_modules/pino-std-serializers": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/probot/node_modules/process-warning": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/probot/node_modules/sonic-boom": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/probot/node_modules/thread-stream": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"real-require": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process": {
|
"node_modules/process": {
|
||||||
"version": "0.11.10",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
@ -15282,11 +15470,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
@ -15359,6 +15548,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -15367,6 +15557,7 @@
|
|||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
@ -15381,6 +15572,7 @@
|
|||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
},
|
},
|
||||||
@ -15961,9 +16153,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@ -15987,6 +16180,7 @@
|
|||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
@ -15994,12 +16188,23 @@
|
|||||||
"node_modules/send/node_modules/debug/node_modules/ms": {
|
"node_modules/send/node_modules/debug/node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send/node_modules/encodeurl": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send/node_modules/mime": {
|
"node_modules/send/node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"mime": "cli.js"
|
"mime": "cli.js"
|
||||||
},
|
},
|
||||||
@ -16013,14 +16218,15 @@
|
|||||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||||
},
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "1.15.0",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"encodeurl": "~1.0.2",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"send": "0.18.0"
|
"send": "0.19.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
@ -16037,14 +16243,17 @@
|
|||||||
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
|
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
|
||||||
},
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.1.1",
|
"define-data-property": "^1.1.4",
|
||||||
"get-intrinsic": "^1.2.1",
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
"gopd": "^1.0.1",
|
"gopd": "^1.0.1",
|
||||||
"has-property-descriptors": "^1.0.0"
|
"has-property-descriptors": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -16103,13 +16312,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.0",
|
"call-bind": "^1.0.7",
|
||||||
"get-intrinsic": "^1.0.2",
|
"es-errors": "^1.3.0",
|
||||||
"object-inspect": "^1.9.0"
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"object-inspect": "^1.13.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@ -17704,6 +17918,7 @@
|
|||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"media-typer": "0.3.0",
|
"media-typer": "0.3.0",
|
||||||
"mime-types": "~2.1.24"
|
"mime-types": "~2.1.24"
|
||||||
@ -17927,6 +18142,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
@ -18051,6 +18267,7 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@
|
|||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"pkijs": "^3.2.4",
|
"pkijs": "^3.2.4",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.3.8",
|
||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||||
|
// add column approverGroupId to AccessApprovalPolicyApprover
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||||
|
// make nullable
|
||||||
|
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
// make approverUserId nullable
|
||||||
|
table.uuid("approverUserId").nullable().alter();
|
||||||
|
});
|
||||||
|
// add column approverGroupId to SecretApprovalPolicyApprover
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||||
|
table.uuid("approverGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
table.uuid("approverUserId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||||
|
// remove
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||||
|
table.dropColumn("approverGroupId");
|
||||||
|
table.uuid("approverUserId").notNullable().alter();
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||||
|
table.dropColumn("approverGroupId");
|
||||||
|
table.uuid("approverUserId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
|
|||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
approverUserId: z.string().uuid()
|
approverUserId: z.string().uuid().nullable().optional(),
|
||||||
|
approverGroupId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||||
|
@ -12,7 +12,8 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
|
|||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
approverUserId: z.string().uuid()
|
approverUserId: z.string().uuid().nullable().optional(),
|
||||||
|
approverGroupId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
@ -11,19 +12,17 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
url: "/",
|
url: "/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
|
||||||
projectSlug: z.string().trim(),
|
projectSlug: z.string().trim(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().trim().default("/"),
|
secretPath: z.string().trim().default("/"),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
approvers: z.string().array().min(1),
|
approvers: z
|
||||||
|
.object({ type: z.nativeEnum(ApproverType), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
})
|
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
|
||||||
path: ["approvals"],
|
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -58,14 +57,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
approvals: sapPubSchema
|
approvals: sapPubSchema
|
||||||
.extend({
|
.extend({
|
||||||
userApprovers: z
|
approvers: z
|
||||||
.object({
|
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
|
||||||
userId: z.string()
|
.array()
|
||||||
})
|
.nullable()
|
||||||
.array(),
|
.optional()
|
||||||
secretPath: z.string().optional().nullable()
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -119,21 +119,19 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
policyId: z.string()
|
policyId: z.string()
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z
|
secretPath: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
.transform((val) => (val === "" ? "/" : val)),
|
||||||
approvers: z.string().array().min(1),
|
approvers: z
|
||||||
approvals: z.number().min(1).default(1),
|
.object({ type: z.nativeEnum(ApproverType), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
|
approvals: z.number().min(1).optional(),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
})
|
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
|
||||||
path: ["approvals"],
|
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -77,6 +77,39 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/entra-id/users",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
|
||||||
|
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
|
||||||
|
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1).describe("The name of the user"),
|
||||||
|
id: z.string().min(1).describe("The ID of the user"),
|
||||||
|
email: z.string().min(1).describe("The email of the user")
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
|
||||||
|
tenantId: req.body.tenantId,
|
||||||
|
applicationId: req.body.applicationId,
|
||||||
|
clientSecret: req.body.clientSecret
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:name",
|
url: "/:name",
|
||||||
@ -237,7 +270,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const dynamicSecretCfgs = await server.services.dynamicSecret.list({
|
const dynamicSecretCfgs = await server.services.dynamicSecret.listDynamicSecretsByEnv({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
@ -10,7 +10,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||||
@ -43,12 +43,59 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:currentSlug",
|
url: "/:id",
|
||||||
method: "PATCH",
|
method: "GET",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
|
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: GroupsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await server.services.group.getGroupById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: GroupsSchema.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const groups = await server.services.org.getOrgGroups({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:id",
|
||||||
|
method: "PATCH",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().trim().describe(GROUPS.UPDATE.id)
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
@ -70,7 +117,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const group = await server.services.group.updateGroup({
|
const group = await server.services.group.updateGroup({
|
||||||
currentSlug: req.params.currentSlug,
|
id: req.params.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@ -83,12 +130,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:slug",
|
url: "/:id",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.DELETE.slug)
|
id: z.string().trim().describe(GROUPS.DELETE.id)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: GroupsSchema
|
200: GroupsSchema
|
||||||
@ -96,7 +143,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const group = await server.services.group.deleteGroup({
|
const group = await server.services.group.deleteGroup({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@ -109,11 +156,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:slug/users",
|
url: "/:id/users",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
|
id: z.string().trim().describe(GROUPS.LIST_USERS.id)
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
||||||
@ -141,24 +188,25 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { users, totalCount } = await server.services.group.listGroupUsers({
|
const { users, totalCount } = await server.services.group.listGroupUsers({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return { users, totalCount };
|
return { users, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:slug/users/:username",
|
url: "/:id/users/:username",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
|
id: z.string().trim().describe(GROUPS.ADD_USER.id),
|
||||||
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -173,7 +221,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.group.addUserToGroup({
|
const user = await server.services.group.addUserToGroup({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
username: req.params.username,
|
username: req.params.username,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@ -187,11 +235,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:slug/users/:username",
|
url: "/:id/users/:username",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
|
id: z.string().trim().describe(GROUPS.DELETE_USER.id),
|
||||||
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -206,7 +254,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.group.removeUserFromGroup({
|
const user = await server.services.group.removeUserFromGroup({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
username: req.params.username,
|
username: req.params.username,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
@ -87,6 +87,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Daniel: This endpoint is no longer is use.
|
||||||
|
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
|
||||||
|
*
|
||||||
|
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
|
||||||
|
*/
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/audit-logs",
|
url: "/:workspaceId/audit-logs",
|
||||||
@ -101,7 +107,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId)
|
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||||
@ -122,10 +128,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
project: z.object({
|
project: z
|
||||||
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string()
|
slug: z.string()
|
||||||
}),
|
})
|
||||||
|
.optional(),
|
||||||
event: z.object({
|
event: z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
metadata: z.any()
|
metadata: z.any()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@ -16,8 +17,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
|
||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@ -27,13 +27,12 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.nullable()
|
.nullable()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||||
approvers: z.string().array().min(1),
|
approvers: z
|
||||||
|
.object({ type: z.nativeEnum(ApproverType), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
})
|
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
|
||||||
path: ["approvals"],
|
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -67,10 +66,12 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
sapId: z.string()
|
sapId: z.string()
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
approvers: z.string().array().min(1),
|
approvers: z
|
||||||
|
.object({ type: z.nativeEnum(ApproverType), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
secretPath: z
|
secretPath: z
|
||||||
.string()
|
.string()
|
||||||
@ -79,10 +80,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
.transform((val) => (val === "" ? "/" : val)),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||||
})
|
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
|
||||||
path: ["approvals"],
|
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -147,9 +144,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
approvals: sapPubSchema
|
approvals: sapPubSchema
|
||||||
.extend({
|
.extend({
|
||||||
userApprovers: z
|
approvers: z
|
||||||
.object({
|
.object({
|
||||||
userId: z.string()
|
id: z.string().nullable().optional(),
|
||||||
|
type: z.nativeEnum(ApproverType)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
@ -186,7 +184,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
policy: sapPubSchema
|
policy: sapPubSchema
|
||||||
.extend({
|
.extend({
|
||||||
userApprovers: z.object({ userId: z.string() }).array()
|
userApprovers: z.object({ userId: z.string().nullable().optional() }).array()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|||||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
const approvalRequestUser = z.object({ userId: z.string() }).merge(
|
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
|
||||||
UsersSchema.pick({
|
UsersSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
firstName: true,
|
firstName: true,
|
||||||
@ -46,7 +46,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: z.string().array(),
|
approvers: z
|
||||||
|
.object({
|
||||||
|
userId: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
enforcementLevel: z.string()
|
enforcementLevel: z.string()
|
||||||
}),
|
}),
|
||||||
@ -54,7 +58,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
|
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
|
||||||
approvers: z.string().array()
|
approvers: z
|
||||||
|
.object({
|
||||||
|
userId: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
}).array()
|
}).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
|
|||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { ApproverType } from "./access-approval-policy-types";
|
||||||
|
|
||||||
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||||
|
|
||||||
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||||
@ -21,6 +23,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||||
@ -30,10 +33,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (policyId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
||||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
[`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
|
||||||
});
|
});
|
||||||
const formattedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: doc,
|
data: doc,
|
||||||
@ -50,9 +53,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "userApprovers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId }) => ({
|
mapper: ({ approverUserId: id }) => ({
|
||||||
userId: approverUserId
|
id,
|
||||||
|
type: "user"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupId: id }) => ({
|
||||||
|
id,
|
||||||
|
type: "group"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -84,9 +96,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "userApprovers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId }) => ({
|
mapper: ({ approverUserId: id }) => ({
|
||||||
userId: approverUserId
|
id,
|
||||||
|
type: ApproverType.User
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupId: id }) => ({
|
||||||
|
id,
|
||||||
|
type: ApproverType.Group
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -7,10 +7,12 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
import { verifyApprovers } from "./access-approval-policy-fns";
|
||||||
import {
|
import {
|
||||||
|
ApproverType,
|
||||||
TCreateAccessApprovalPolicy,
|
TCreateAccessApprovalPolicy,
|
||||||
TDeleteAccessApprovalPolicy,
|
TDeleteAccessApprovalPolicy,
|
||||||
TGetAccessPolicyCountByEnvironmentDTO,
|
TGetAccessPolicyCountByEnvironmentDTO,
|
||||||
@ -25,6 +27,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
|
groupDAL: TGroupDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||||
@ -32,6 +35,7 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
|
|||||||
export const accessApprovalPolicyServiceFactory = ({
|
export const accessApprovalPolicyServiceFactory = ({
|
||||||
accessApprovalPolicyDAL,
|
accessApprovalPolicyDAL,
|
||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
|
groupDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL
|
projectDAL
|
||||||
@ -52,7 +56,15 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
if (approvals > approvers.length)
|
// If there is a group approver people might be added to the group later to meet the approvers quota
|
||||||
|
const groupApprovers = approvers
|
||||||
|
.filter((approver) => approver.type === ApproverType.Group)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
const userApprovers = approvers
|
||||||
|
.filter((approver) => approver.type === ApproverType.User)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
|
||||||
|
if (!groupApprovers && approvals > userApprovers.length)
|
||||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -69,6 +81,24 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||||
|
|
||||||
|
const verifyAllApprovers = userApprovers;
|
||||||
|
const usersPromises: Promise<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
email: string | null | undefined;
|
||||||
|
username: string;
|
||||||
|
firstName: string | null | undefined;
|
||||||
|
lastName: string | null | undefined;
|
||||||
|
isPartOfGroup: boolean;
|
||||||
|
}[]
|
||||||
|
>[] = [];
|
||||||
|
|
||||||
|
for (const groupId of groupApprovers) {
|
||||||
|
usersPromises.push(groupDAL.findAllGroupMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||||
|
}
|
||||||
|
const verifyGroupApprovers = (await Promise.all(usersPromises)).flat().map((user) => user.id);
|
||||||
|
verifyAllApprovers.push(...verifyGroupApprovers);
|
||||||
|
|
||||||
await verifyApprovers({
|
await verifyApprovers({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
@ -76,7 +106,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
permissionService,
|
permissionService,
|
||||||
userIds: approvers
|
userIds: verifyAllApprovers
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
@ -90,13 +120,26 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
if (userApprovers) {
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
approvers.map((userId) => ({
|
userApprovers.map((userId) => ({
|
||||||
approverUserId: userId,
|
approverUserId: userId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupApprovers) {
|
||||||
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
|
groupApprovers.map((groupId) => ({
|
||||||
|
approverGroupId: groupId,
|
||||||
|
policyId: doc.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return { ...accessApproval, environment: env, projectId: project.id };
|
return { ...accessApproval, environment: env, projectId: project.id };
|
||||||
@ -138,7 +181,19 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
approvals,
|
approvals,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
}: TUpdateAccessApprovalPolicy) => {
|
}: TUpdateAccessApprovalPolicy) => {
|
||||||
|
const groupApprovers = approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
const userApprovers = approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.User)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
|
||||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||||
|
const currentAppovals = approvals || accessApprovalPolicy.approvals;
|
||||||
|
if (groupApprovers?.length === 0 && userApprovers && currentAppovals > userApprovers.length) {
|
||||||
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
|
}
|
||||||
|
|
||||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -161,7 +216,10 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
if (approvers) {
|
|
||||||
|
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||||
|
|
||||||
|
if (userApprovers) {
|
||||||
await verifyApprovers({
|
await verifyApprovers({
|
||||||
projectId: accessApprovalPolicy.projectId,
|
projectId: accessApprovalPolicy.projectId,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
@ -169,18 +227,52 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
secretPath: doc.secretPath!,
|
secretPath: doc.secretPath!,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
permissionService,
|
permissionService,
|
||||||
userIds: approvers
|
userIds: userApprovers
|
||||||
});
|
});
|
||||||
|
|
||||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
approvers.map((userId) => ({
|
userApprovers.map((userId) => ({
|
||||||
approverUserId: userId,
|
approverUserId: userId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (groupApprovers) {
|
||||||
|
const usersPromises: Promise<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
email: string | null | undefined;
|
||||||
|
username: string;
|
||||||
|
firstName: string | null | undefined;
|
||||||
|
lastName: string | null | undefined;
|
||||||
|
isPartOfGroup: boolean;
|
||||||
|
}[]
|
||||||
|
>[] = [];
|
||||||
|
|
||||||
|
for (const groupId of groupApprovers) {
|
||||||
|
usersPromises.push(groupDAL.findAllGroupMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||||
|
}
|
||||||
|
const verifyGroupApprovers = (await Promise.all(usersPromises)).flat().map((user) => user.id);
|
||||||
|
|
||||||
|
await verifyApprovers({
|
||||||
|
projectId: accessApprovalPolicy.projectId,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
envSlug: accessApprovalPolicy.environment.slug,
|
||||||
|
secretPath: doc.secretPath!,
|
||||||
|
actorAuthMethod,
|
||||||
|
permissionService,
|
||||||
|
userIds: verifyGroupApprovers
|
||||||
|
});
|
||||||
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
|
groupApprovers.map((groupId) => ({
|
||||||
|
approverGroupId: groupId,
|
||||||
|
policyId: doc.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
@ -13,11 +13,16 @@ export type TVerifyApprovers = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ApproverType {
|
||||||
|
Group = "group",
|
||||||
|
User = "user"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCreateAccessApprovalPolicy = {
|
export type TCreateAccessApprovalPolicy = {
|
||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: string[];
|
approvers: { type: ApproverType; id: string }[];
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@ -26,7 +31,7 @@ export type TCreateAccessApprovalPolicy = {
|
|||||||
export type TUpdateAccessApprovalPolicy = {
|
export type TUpdateAccessApprovalPolicy = {
|
||||||
policyId: string;
|
policyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
approvers?: string[];
|
approvers?: { type: ApproverType; id: string }[];
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
@ -39,6 +39,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
|
||||||
.join<TUsers>(
|
.join<TUsers>(
|
||||||
db(TableName.Users).as("requestedByUser"),
|
db(TableName.Users).as("requestedByUser"),
|
||||||
@ -59,6 +65,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
|
||||||
|
|
||||||
.select(
|
.select(
|
||||||
db.ref("projectId").withSchema(TableName.Environment),
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
@ -142,7 +149,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
||||||
},
|
},
|
||||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
|
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
|
||||||
|
{
|
||||||
|
key: "approverGroupUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupUserId }) => approverGroupUserId
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,17 +184,28 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`requestedByUser.id`
|
`requestedByUser.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.AccessApprovalPolicyApprover,
|
TableName.AccessApprovalPolicyApprover,
|
||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
|
||||||
.join<TUsers>(
|
.leftJoin<TUsers>(
|
||||||
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
||||||
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||||
"accessApprovalPolicyApproverUser.id"
|
"accessApprovalPolicyApproverUser.id"
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
|
||||||
|
`${TableName.UserGroupMembership}.userId`,
|
||||||
|
"accessApprovalPolicyGroupApproverUser.id"
|
||||||
|
)
|
||||||
|
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.AccessApprovalRequestReviewer,
|
TableName.AccessApprovalRequestReviewer,
|
||||||
@ -200,10 +223,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
||||||
|
tx.ref("userId").withSchema(TableName.UserGroupMembership),
|
||||||
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
|
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||||
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
|
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
|
||||||
|
tx.ref("username").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||||
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
|
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
|
||||||
|
tx.ref("firstName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
|
||||||
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
|
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
|
||||||
|
tx.ref("lastName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
|
||||||
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
||||||
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
||||||
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
||||||
@ -282,6 +310,23 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
lastName,
|
lastName,
|
||||||
username
|
username
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({
|
||||||
|
userId,
|
||||||
|
approverGroupEmail: email,
|
||||||
|
approverGroupUsername: username,
|
||||||
|
approverGroupLastName: lastName,
|
||||||
|
approverFirstName: firstName
|
||||||
|
}) => ({
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
username
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
|||||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||||
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||||
@ -57,6 +58,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
TAccessApprovalRequestReviewerDALFactory,
|
TAccessApprovalRequestReviewerDALFactory,
|
||||||
"create" | "find" | "findOne" | "transaction"
|
"create" | "find" | "findOne" | "transaction"
|
||||||
>;
|
>;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findAllGroupMembers">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
@ -70,6 +72,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||||
|
|
||||||
export const accessApprovalRequestServiceFactory = ({
|
export const accessApprovalRequestServiceFactory = ({
|
||||||
|
groupDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@ -124,13 +127,36 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
|
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
|
||||||
|
|
||||||
|
const approverIds: string[] = [];
|
||||||
|
const approverGroupIds: string[] = [];
|
||||||
|
|
||||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||||
policyId: policy.id
|
policyId: policy.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
approvers.forEach((approver) => {
|
||||||
|
if (approver.approverUserId) {
|
||||||
|
approverIds.push(approver.approverUserId);
|
||||||
|
} else if (approver.approverGroupId) {
|
||||||
|
approverGroupIds.push(approver.approverGroupId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupUsers = (
|
||||||
|
await Promise.all(
|
||||||
|
approverGroupIds.map((groupApproverId) =>
|
||||||
|
groupDAL.findAllGroupMembers({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
groupId: groupApproverId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
|
approverIds.push(...groupUsers.map((user) => user.id));
|
||||||
|
|
||||||
const approverUsers = await userDAL.find({
|
const approverUsers = await userDAL.find({
|
||||||
$in: {
|
$in: {
|
||||||
id: approvers.map((approver) => approver.approverUserId)
|
id: [...new Set(approverIds)]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { Knex } from "knex";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { AuditLogsSchema, TableName } from "@app/db/schemas";
|
import { AuditLogsSchema, TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@ -48,47 +48,61 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
},
|
},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
|
if (!orgId && !projectId) {
|
||||||
|
throw new Error("Either orgId or projectId must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Find statements
|
||||||
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
||||||
.where(
|
|
||||||
stripUndefinedInWhere({
|
|
||||||
projectId,
|
|
||||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
|
||||||
userAgentType
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
.where(function () {
|
||||||
|
if (orgId) {
|
||||||
|
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId);
|
||||||
|
} else if (projectId) {
|
||||||
|
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userAgentType) {
|
||||||
|
void sqlQuery.where("userAgentType", userAgentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select statements
|
||||||
|
void sqlQuery
|
||||||
.select(selectAllTableCols(TableName.AuditLog))
|
.select(selectAllTableCols(TableName.AuditLog))
|
||||||
|
|
||||||
.select(
|
.select(
|
||||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
||||||
)
|
)
|
||||||
|
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||||
|
|
||||||
|
// Special case: Filter by actor ID
|
||||||
if (actorId) {
|
if (actorId) {
|
||||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
|
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special case: Filter by key/value pairs in eventMetadata field
|
||||||
if (eventMetadata && Object.keys(eventMetadata).length) {
|
if (eventMetadata && Object.keys(eventMetadata).length) {
|
||||||
Object.entries(eventMetadata).forEach(([key, value]) => {
|
Object.entries(eventMetadata).forEach(([key, value]) => {
|
||||||
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
|
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by actor type
|
||||||
if (actorType) {
|
if (actorType) {
|
||||||
void sqlQuery.where("actor", actorType);
|
void sqlQuery.where("actor", actorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by event types
|
||||||
if (eventType?.length) {
|
if (eventType?.length) {
|
||||||
void sqlQuery.whereIn("eventType", eventType);
|
void sqlQuery.whereIn("eventType", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by date range
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||||
}
|
}
|
||||||
@ -97,13 +111,21 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
const docs = await sqlQuery;
|
const docs = await sqlQuery;
|
||||||
|
|
||||||
return docs.map((doc) => ({
|
return docs.map((doc) => {
|
||||||
|
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
|
||||||
|
// This is a quick and dirty way to get around the types.
|
||||||
|
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
|
||||||
|
|
||||||
|
return {
|
||||||
...AuditLogsSchema.parse(doc),
|
...AuditLogsSchema.parse(doc),
|
||||||
|
...(projectDoc?.projectSlug && {
|
||||||
project: {
|
project: {
|
||||||
name: doc.projectName,
|
name: projectDoc.projectName,
|
||||||
slug: doc.projectSlug
|
slug: projectDoc.projectSlug
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error });
|
throw new DatabaseError({ error });
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ export const auditLogServiceFactory = ({
|
|||||||
permissionService
|
permissionService
|
||||||
}: TAuditLogServiceFactoryDep) => {
|
}: TAuditLogServiceFactoryDep) => {
|
||||||
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
||||||
|
// Filter logs for specific project
|
||||||
if (filter.projectId) {
|
if (filter.projectId) {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -34,6 +35,7 @@ export const auditLogServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
} else {
|
} else {
|
||||||
|
// Organization-wide logs
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -44,13 +46,12 @@ export const auditLogServiceFactory = ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
||||||
* to the organization level
|
* to the organization level ✅
|
||||||
*/
|
*/
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||||
|
|
||||||
const auditLogs = await auditLogDAL.find({
|
const auditLogs = await auditLogDAL.find({
|
||||||
startDate: filter.startDate,
|
startDate: filter.startDate,
|
||||||
endDate: filter.endDate,
|
endDate: filter.endDate,
|
||||||
|
@ -1,10 +1,70 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
||||||
|
|
||||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||||
const orm = ormify(db, TableName.DynamicSecret);
|
const orm = ormify(db, TableName.DynamicSecret);
|
||||||
return orm;
|
|
||||||
|
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||||
|
const listDynamicSecretsByFolderIds = async (
|
||||||
|
{
|
||||||
|
folderIds,
|
||||||
|
search,
|
||||||
|
limit,
|
||||||
|
offset = 0,
|
||||||
|
orderBy = SecretsOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC
|
||||||
|
}: {
|
||||||
|
folderIds: string[];
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||||
|
.whereIn("folderId", folderIds)
|
||||||
|
.where((bd) => {
|
||||||
|
if (search) {
|
||||||
|
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||||
|
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.DynamicSecret),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
|
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
const rankOffset = offset + 1;
|
||||||
|
return await (tx || db)
|
||||||
|
.with("w", query)
|
||||||
|
.select("*")
|
||||||
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
|
.where("w.rank", ">=", rankOffset)
|
||||||
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicSecrets = await query;
|
||||||
|
|
||||||
|
return dynamicSecrets;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "List dynamic secret multi env" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...orm, listDynamicSecretsByFolderIds };
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
@ -17,9 +18,12 @@ import {
|
|||||||
TCreateDynamicSecretDTO,
|
TCreateDynamicSecretDTO,
|
||||||
TDeleteDynamicSecretDTO,
|
TDeleteDynamicSecretDTO,
|
||||||
TDetailsDynamicSecretDTO,
|
TDetailsDynamicSecretDTO,
|
||||||
|
TGetDynamicSecretsCountDTO,
|
||||||
TListDynamicSecretsDTO,
|
TListDynamicSecretsDTO,
|
||||||
|
TListDynamicSecretsMultiEnvDTO,
|
||||||
TUpdateDynamicSecretDTO
|
TUpdateDynamicSecretDTO
|
||||||
} from "./dynamic-secret-types";
|
} from "./dynamic-secret-types";
|
||||||
|
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
|
||||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||||
|
|
||||||
type TDynamicSecretServiceFactoryDep = {
|
type TDynamicSecretServiceFactoryDep = {
|
||||||
@ -31,7 +35,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
||||||
>;
|
>;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
@ -300,19 +304,55 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const list = async ({
|
// get unique dynamic secret count across multiple envs
|
||||||
|
const getCountMultiEnv = async ({
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
projectSlug,
|
projectId,
|
||||||
path,
|
path,
|
||||||
environmentSlug
|
environmentSlugs,
|
||||||
}: TListDynamicSecretsDTO) => {
|
search
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
const projectId = project.id;
|
// verify user has access to each env in request
|
||||||
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
|
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||||
|
{ $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
|
{ countDistinct: "name" }
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// get dynamic secret count for a single env
|
||||||
|
const getDynamicSecretCount = async ({
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
path,
|
||||||
|
environmentSlug,
|
||||||
|
search,
|
||||||
|
projectId
|
||||||
|
}: TGetDynamicSecretsCountDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -328,15 +368,127 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||||
|
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
|
{ count: true }
|
||||||
|
);
|
||||||
|
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listDynamicSecretsByEnv = async ({
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
projectSlug,
|
||||||
|
path,
|
||||||
|
environmentSlug,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
search,
|
||||||
|
...params
|
||||||
|
}: TListDynamicSecretsDTO) => {
|
||||||
|
let { projectId } = params;
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
projectId = project.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||||
|
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
||||||
|
}
|
||||||
|
);
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// get dynamic secrets for multiple envs
|
||||||
|
const listDynamicSecretsByFolderIds = async ({
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
path,
|
||||||
|
environmentSlugs,
|
||||||
|
projectId,
|
||||||
|
...params
|
||||||
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify user has access to each env in request
|
||||||
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
|
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
|
||||||
|
folderIds: folders.map((folder) => folder.id),
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
|
||||||
|
return dynamicSecretCfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAzureEntraIdUsers = async ({
|
||||||
|
tenantId,
|
||||||
|
applicationId,
|
||||||
|
clientSecret
|
||||||
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
applicationId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
}) => {
|
||||||
|
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
|
||||||
|
tenantId,
|
||||||
|
applicationId,
|
||||||
|
clientSecret
|
||||||
|
);
|
||||||
|
return azureEntraIdUsers;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
updateByName,
|
updateByName,
|
||||||
deleteByName,
|
deleteByName,
|
||||||
getDetails,
|
getDetails,
|
||||||
list
|
listDynamicSecretsByEnv,
|
||||||
|
listDynamicSecretsByFolderIds,
|
||||||
|
getDynamicSecretCount,
|
||||||
|
getCountMultiEnv,
|
||||||
|
fetchAzureEntraIdUsers
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||||
|
|
||||||
@ -50,5 +51,20 @@ export type TDetailsDynamicSecretDTO = {
|
|||||||
export type TListDynamicSecretsDTO = {
|
export type TListDynamicSecretsDTO = {
|
||||||
path: string;
|
path: string;
|
||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
projectSlug: string;
|
projectSlug?: string;
|
||||||
|
projectId?: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListDynamicSecretsMultiEnvDTO = Omit<
|
||||||
|
TListDynamicSecretsDTO,
|
||||||
|
"projectId" | "environmentSlug" | "projectSlug"
|
||||||
|
> & { projectId: string; environmentSlugs: string[] };
|
||||||
|
|
||||||
|
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,138 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
|
||||||
|
const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
type User = { name: string; id: string; email: string };
|
||||||
|
|
||||||
|
export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||||
|
fetchAzureEntraIdUsers: (tenantId: string, applicationId: string, clientSecret: string) => Promise<User[]>;
|
||||||
|
} => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await AzureEntraIDSchema.parseAsync(inputs);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToken = async (
|
||||||
|
tenantId: string,
|
||||||
|
applicationId: string,
|
||||||
|
clientSecret: string
|
||||||
|
): Promise<{ token?: string; success: boolean }> => {
|
||||||
|
const response = await axios.post<{ access_token: string }>(
|
||||||
|
`${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`,
|
||||||
|
{
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: applicationId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: "https://graph.microsoft.com/.default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { token: response.data.access_token, success: true };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||||
|
return data.success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Do nothing
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||||
|
if (!data.success) {
|
||||||
|
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = generatePassword();
|
||||||
|
|
||||||
|
const response = await axios.patch(
|
||||||
|
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||||
|
{
|
||||||
|
passwordProfile: {
|
||||||
|
forceChangePasswordNextSignIn: false,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${data.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.status !== 204) {
|
||||||
|
throw new BadRequestError({ message: "Failed to update password" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Creates a new password
|
||||||
|
await create(inputs);
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||||
|
const data = await getToken(tenantId, applicationId, clientSecret);
|
||||||
|
if (!data.success) {
|
||||||
|
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>(
|
||||||
|
`${MSFT_GRAPH_API_URL}/users`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Authorization: `Bearer ${data.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new BadRequestError({ message: "Failed to fetch users" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = response.data.value.map((user) => {
|
||||||
|
return {
|
||||||
|
name: user.displayName,
|
||||||
|
id: user.id,
|
||||||
|
email: user.userPrincipalName
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew,
|
||||||
|
fetchAzureEntraIdUsers
|
||||||
|
};
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { ElasticSearchProvider } from "./elastic-search";
|
import { ElasticSearchProvider } from "./elastic-search";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
@ -18,5 +19,6 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
||||||
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
||||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider()
|
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||||
|
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider()
|
||||||
});
|
});
|
||||||
|
@ -166,6 +166,14 @@ export const DynamicSecretMongoDBSchema = z.object({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AzureEntraIDSchema = z.object({
|
||||||
|
tenantId: z.string().trim().min(1),
|
||||||
|
userId: z.string().trim().min(1),
|
||||||
|
email: z.string().trim().min(1),
|
||||||
|
applicationId: z.string().trim().min(1),
|
||||||
|
clientSecret: z.string().trim().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra",
|
Cassandra = "cassandra",
|
||||||
@ -175,7 +183,8 @@ export enum DynamicSecretProviders {
|
|||||||
MongoAtlas = "mongo-db-atlas",
|
MongoAtlas = "mongo-db-atlas",
|
||||||
ElasticSearch = "elastic-search",
|
ElasticSearch = "elastic-search",
|
||||||
MongoDB = "mongo-db",
|
MongoDB = "mongo-db",
|
||||||
RabbitMq = "rabbit-mq"
|
RabbitMq = "rabbit-mq",
|
||||||
|
AzureEntraID = "azure-entra-id"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
@ -187,7 +196,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
|
|
||||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
TAddUserToGroupDTO,
|
TAddUserToGroupDTO,
|
||||||
TCreateGroupDTO,
|
TCreateGroupDTO,
|
||||||
TDeleteGroupDTO,
|
TDeleteGroupDTO,
|
||||||
|
TGetGroupByIdDTO,
|
||||||
TListGroupUsersDTO,
|
TListGroupUsersDTO,
|
||||||
TRemoveUserFromGroupDTO,
|
TRemoveUserFromGroupDTO,
|
||||||
TUpdateGroupDTO
|
TUpdateGroupDTO
|
||||||
@ -29,7 +30,7 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
|||||||
|
|
||||||
type TGroupServiceFactoryDep = {
|
type TGroupServiceFactoryDep = {
|
||||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
||||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
|
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "findById">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
|
||||||
userGroupMembershipDAL: Pick<
|
userGroupMembershipDAL: Pick<
|
||||||
@ -95,7 +96,7 @@ export const groupServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateGroup = async ({
|
const updateGroup = async ({
|
||||||
currentSlug,
|
id,
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
role,
|
role,
|
||||||
@ -121,8 +122,10 @@ export const groupServiceFactory = ({
|
|||||||
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
||||||
});
|
});
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
|
const group = await groupDAL.findOne({ orgId: actorOrgId, id });
|
||||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
|
if (!group) {
|
||||||
|
throw new BadRequestError({ message: `Failed to find group with ID ${id}` });
|
||||||
|
}
|
||||||
|
|
||||||
let customRole: TOrgRoles | undefined;
|
let customRole: TOrgRoles | undefined;
|
||||||
if (role) {
|
if (role) {
|
||||||
@ -140,8 +143,7 @@ export const groupServiceFactory = ({
|
|||||||
|
|
||||||
const [updatedGroup] = await groupDAL.update(
|
const [updatedGroup] = await groupDAL.update(
|
||||||
{
|
{
|
||||||
orgId: actorOrgId,
|
id: group.id
|
||||||
slug: currentSlug
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@ -158,7 +160,7 @@ export const groupServiceFactory = ({
|
|||||||
return updatedGroup;
|
return updatedGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
@ -178,15 +180,39 @@ export const groupServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [group] = await groupDAL.delete({
|
const [group] = await groupDAL.delete({
|
||||||
orgId: actorOrgId,
|
id,
|
||||||
slug: groupSlug
|
orgId: actorOrgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
|
||||||
|
if (!actorOrgId) {
|
||||||
|
throw new BadRequestError({ message: "Failed to read group without organization" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
const group = await groupDAL.findById(id);
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Cannot find group with ID ${id}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
};
|
||||||
|
|
||||||
const listGroupUsers = async ({
|
const listGroupUsers = async ({
|
||||||
groupSlug,
|
id,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
username,
|
username,
|
||||||
@ -208,12 +234,12 @@ export const groupServiceFactory = ({
|
|||||||
|
|
||||||
const group = await groupDAL.findOne({
|
const group = await groupDAL.findOne({
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
slug: groupSlug
|
id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group)
|
if (!group)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Failed to find group with slug ${groupSlug}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = await groupDAL.findAllGroupMembers({
|
const users = await groupDAL.findAllGroupMembers({
|
||||||
@ -229,14 +255,7 @@ export const groupServiceFactory = ({
|
|||||||
return { users, totalCount: count };
|
return { users, totalCount: count };
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUserToGroup = async ({
|
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||||
groupSlug,
|
|
||||||
username,
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
}: TAddUserToGroupDTO) => {
|
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
@ -251,12 +270,12 @@ export const groupServiceFactory = ({
|
|||||||
// check if group with slug exists
|
// check if group with slug exists
|
||||||
const group = await groupDAL.findOne({
|
const group = await groupDAL.findOne({
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
slug: groupSlug
|
id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group)
|
if (!group)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Failed to find group with slug ${groupSlug}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
@ -285,7 +304,7 @@ export const groupServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeUserFromGroup = async ({
|
const removeUserFromGroup = async ({
|
||||||
groupSlug,
|
id,
|
||||||
username,
|
username,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -306,12 +325,12 @@ export const groupServiceFactory = ({
|
|||||||
// check if group with slug exists
|
// check if group with slug exists
|
||||||
const group = await groupDAL.findOne({
|
const group = await groupDAL.findOne({
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
slug: groupSlug
|
id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group)
|
if (!group)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Failed to find group with slug ${groupSlug}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
@ -342,6 +361,7 @@ export const groupServiceFactory = ({
|
|||||||
deleteGroup,
|
deleteGroup,
|
||||||
listGroupUsers,
|
listGroupUsers,
|
||||||
addUserToGroup,
|
addUserToGroup,
|
||||||
removeUserFromGroup
|
removeUserFromGroup,
|
||||||
|
getGroupById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export type TCreateGroupDTO = {
|
|||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TUpdateGroupDTO = {
|
export type TUpdateGroupDTO = {
|
||||||
currentSlug: string;
|
id: string;
|
||||||
} & Partial<{
|
} & Partial<{
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -26,23 +26,27 @@ export type TUpdateGroupDTO = {
|
|||||||
TGenericPermission;
|
TGenericPermission;
|
||||||
|
|
||||||
export type TDeleteGroupDTO = {
|
export type TDeleteGroupDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
export type TGetGroupByIdDTO = {
|
||||||
|
id: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TListGroupUsersDTO = {
|
export type TListGroupUsersDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TAddUserToGroupDTO = {
|
export type TAddUserToGroupDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TRemoveUserFromGroupDTO = {
|
export type TRemoveUserFromGroupDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
|
||||||
|
|
||||||
export enum OrgPermissionActions {
|
export enum OrgPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@ -27,7 +25,8 @@ export enum OrgPermissionSubjects {
|
|||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console"
|
AdminConsole = "organization-admin-console",
|
||||||
|
AuditLogs = "audit-logs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@ -45,10 +44,11 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
// ws permissions
|
// ws permissions
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
@ -113,15 +113,20 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgAdminPermissions = buildAdminPermission();
|
export const orgAdminPermissions = buildAdminPermission();
|
||||||
|
|
||||||
const buildMemberPermission = () => {
|
const buildMemberPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
@ -142,14 +147,16 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgMemberPermissions = buildMemberPermission();
|
export const orgMemberPermissions = buildMemberPermission();
|
||||||
|
|
||||||
const buildNoAccessPermission = () => {
|
const buildNoAccessPermission = () => {
|
||||||
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgNoAccessPermissions = buildNoAccessPermission();
|
export const orgNoAccessPermissions = buildNoAccessPermission();
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas";
|
import {
|
||||||
|
IdentityProjectMembershipRoleSchema,
|
||||||
|
OrgMembershipsSchema,
|
||||||
|
TableName,
|
||||||
|
TProjectRoles,
|
||||||
|
TProjects
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
@ -10,18 +16,91 @@ export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
|
|||||||
export const permissionDALFactory = (db: TDbClient) => {
|
export const permissionDALFactory = (db: TDbClient) => {
|
||||||
const getOrgPermission = async (userId: string, orgId: string) => {
|
const getOrgPermission = async (userId: string, orgId: string) => {
|
||||||
try {
|
try {
|
||||||
|
const groupSubQuery = db(TableName.Groups)
|
||||||
|
.where(`${TableName.Groups}.orgId`, orgId)
|
||||||
|
.join(TableName.UserGroupMembership, (queryBuilder) => {
|
||||||
|
queryBuilder
|
||||||
|
.on(`${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.andOn(`${TableName.UserGroupMembership}.userId`, db.raw("?", [userId]));
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Groups).as("groupOrgId"),
|
||||||
|
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug"),
|
||||||
|
db.ref("role").withSchema(TableName.Groups).as("groupRole"),
|
||||||
|
db.ref("roleId").withSchema(TableName.Groups).as("groupRoleId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.Groups).as("groupCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.Groups).as("groupUpdatedAt"),
|
||||||
|
db.ref("permissions").withSchema(TableName.OrgRoles).as("groupCustomRolePermission")
|
||||||
|
);
|
||||||
|
|
||||||
const membership = await db
|
const membership = await db
|
||||||
.replicaNode()(TableName.OrgMembership)
|
.replicaNode()(TableName.OrgMembership)
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
|
||||||
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
|
||||||
.where("userId", userId)
|
|
||||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
|
.where(`${TableName.OrgMembership}.userId`, userId)
|
||||||
.select("permissions")
|
.leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`)
|
||||||
.select(selectAllTableCols(TableName.OrgMembership))
|
.leftJoin<Awaited<typeof groupSubQuery>[0]>(
|
||||||
.first();
|
groupSubQuery.as("userGroups"),
|
||||||
|
"userGroups.groupOrgId",
|
||||||
|
db.raw("?", [orgId])
|
||||||
|
)
|
||||||
|
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.OrgMembership),
|
||||||
|
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("groupId").withSchema("userGroups"),
|
||||||
|
db.ref("groupOrgId").withSchema("userGroups"),
|
||||||
|
db.ref("groupName").withSchema("userGroups"),
|
||||||
|
db.ref("groupSlug").withSchema("userGroups"),
|
||||||
|
db.ref("groupRole").withSchema("userGroups"),
|
||||||
|
db.ref("groupRoleId").withSchema("userGroups"),
|
||||||
|
db.ref("groupCreatedAt").withSchema("userGroups"),
|
||||||
|
db.ref("groupUpdatedAt").withSchema("userGroups"),
|
||||||
|
db.ref("groupCustomRolePermission").withSchema("userGroups")
|
||||||
|
);
|
||||||
|
|
||||||
return membership;
|
const [formatedDoc] = sqlNestRelationships({
|
||||||
|
data: membership,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) =>
|
||||||
|
OrgMembershipsSchema.extend({
|
||||||
|
permissions: z.unknown(),
|
||||||
|
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||||
|
customRoleSlug: z.string().optional().nullable()
|
||||||
|
}).parse(el),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "groupId",
|
||||||
|
label: "groups" as const,
|
||||||
|
mapper: ({
|
||||||
|
groupId,
|
||||||
|
groupUpdatedAt,
|
||||||
|
groupCreatedAt,
|
||||||
|
groupRole,
|
||||||
|
groupRoleId,
|
||||||
|
groupCustomRolePermission,
|
||||||
|
groupName,
|
||||||
|
groupSlug,
|
||||||
|
groupOrgId
|
||||||
|
}) => ({
|
||||||
|
id: groupId,
|
||||||
|
updatedAt: groupUpdatedAt,
|
||||||
|
createdAt: groupCreatedAt,
|
||||||
|
role: groupRole,
|
||||||
|
roleId: groupRoleId,
|
||||||
|
customRolePermission: groupCustomRolePermission,
|
||||||
|
name: groupName,
|
||||||
|
slug: groupSlug,
|
||||||
|
orgId: groupOrgId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatedDoc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetOrgPermission" });
|
throw new DatabaseError({ error, name: "GetOrgPermission" });
|
||||||
}
|
}
|
||||||
@ -47,74 +126,31 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const groups: string[] = await db
|
const docs = await db
|
||||||
.replicaNode()(TableName.GroupProjectMembership)
|
.replicaNode()(TableName.Users)
|
||||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
.where(`${TableName.Users}.id`, userId)
|
||||||
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
||||||
const groupDocs = await db
|
void queryBuilder
|
||||||
.replicaNode()(TableName.UserGroupMembership)
|
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
|
||||||
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
|
})
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.GroupProjectMembership,
|
|
||||||
`${TableName.GroupProjectMembership}.groupId`,
|
|
||||||
`${TableName.UserGroupMembership}.groupId`
|
|
||||||
)
|
|
||||||
.join(
|
|
||||||
TableName.GroupProjectMembershipRole,
|
TableName.GroupProjectMembershipRole,
|
||||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.GroupProjectMembership}.id`
|
`${TableName.GroupProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin<TProjectRoles>(
|
||||||
.leftJoin(
|
{ groupCustomRoles: TableName.ProjectRoles },
|
||||||
TableName.ProjectRoles,
|
|
||||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`groupCustomRoles.id`
|
||||||
)
|
)
|
||||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.leftJoin(TableName.ProjectMembership, (queryBuilder) => {
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
void queryBuilder
|
||||||
|
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||||
|
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
|
||||||
|
})
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectUserAdditionalPrivilege,
|
|
||||||
`${TableName.GroupProjectMembership}.projectId`,
|
|
||||||
`${TableName.Project}.id`
|
|
||||||
)
|
|
||||||
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
|
||||||
.select(
|
|
||||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
|
||||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
|
|
||||||
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
|
|
||||||
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
|
||||||
|
|
||||||
db.ref("permissions").withSchema(TableName.ProjectRoles).as("permissions"),
|
|
||||||
// db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("apPermissions")
|
|
||||||
// Additional Privileges
|
|
||||||
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
|
|
||||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
|
|
||||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
|
||||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
|
||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
|
||||||
|
|
||||||
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
|
|
||||||
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
|
|
||||||
|
|
||||||
db
|
|
||||||
.ref("temporaryAccessStartTime")
|
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
|
||||||
.as("userApTemporaryAccessStartTime"),
|
|
||||||
db
|
|
||||||
.ref("temporaryAccessEndTime")
|
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
|
||||||
.as("userApTemporaryAccessEndTime")
|
|
||||||
);
|
|
||||||
// .select(`${TableName.ProjectRoles}.permissions`);
|
|
||||||
|
|
||||||
const docs = await db(TableName.ProjectMembership)
|
|
||||||
.join(
|
|
||||||
TableName.ProjectUserMembershipRole,
|
TableName.ProjectUserMembershipRole,
|
||||||
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.ProjectMembership}.id`
|
`${TableName.ProjectMembership}.id`
|
||||||
@ -124,176 +160,229 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`${TableName.ProjectRoles}.id`
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
|
||||||
TableName.ProjectUserAdditionalPrivilege,
|
void queryBuilder
|
||||||
`${TableName.ProjectUserAdditionalPrivilege}.projectId`,
|
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
|
||||||
`${TableName.ProjectMembership}.projectId`
|
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
|
||||||
)
|
})
|
||||||
|
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
.where(`${TableName.ProjectMembership}.userId`, userId)
|
|
||||||
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
|
||||||
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
|
||||||
.select(
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
// groups specific
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("customRoleId")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleCustomRoleId"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
// user specific
|
||||||
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||||
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
|
||||||
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
db
|
||||||
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
|
.ref("temporaryMode")
|
||||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
.as("userProjectMembershipRoleTemporaryMode"),
|
||||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
db
|
||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
|
.as("userProjectMembershipRoleIsTemporary"),
|
||||||
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
|
||||||
|
db
|
||||||
|
.ref("permissions")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesPermissions"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryRange"),
|
||||||
|
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
|
||||||
db
|
db
|
||||||
.ref("temporaryAccessStartTime")
|
.ref("temporaryAccessStartTime")
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
.as("userApTemporaryAccessStartTime"),
|
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
|
||||||
db
|
db
|
||||||
.ref("temporaryAccessEndTime")
|
.ref("temporaryAccessEndTime")
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
.as("userApTemporaryAccessEndTime")
|
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||||
|
// general
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
);
|
);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const [userPermission] = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "projectId",
|
key: "projectId",
|
||||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
parentMapper: ({
|
||||||
|
orgId,
|
||||||
|
orgAuthEnforced,
|
||||||
|
membershipId,
|
||||||
|
groupMembershipId,
|
||||||
|
membershipCreatedAt,
|
||||||
|
groupMembershipCreatedAt,
|
||||||
|
groupMembershipUpdatedAt,
|
||||||
|
membershipUpdatedAt
|
||||||
|
}) => ({
|
||||||
orgId,
|
orgId,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
userId,
|
userId,
|
||||||
id: membershipId,
|
|
||||||
projectId,
|
projectId,
|
||||||
createdAt: membershipCreatedAt,
|
id: membershipId || groupMembershipId,
|
||||||
updatedAt: membershipUpdatedAt
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "id",
|
key: "userGroupProjectMembershipRoleId",
|
||||||
label: "roles" as const,
|
label: "userGroupRoles" as const,
|
||||||
mapper: (data) =>
|
mapper: ({
|
||||||
ProjectUserMembershipRolesSchema.extend({
|
userGroupProjectMembershipRoleId,
|
||||||
permissions: z.unknown(),
|
userGroupProjectMembershipRole,
|
||||||
customRoleSlug: z.string().optional().nullable()
|
userGroupProjectMembershipRolePermission,
|
||||||
}).parse(data)
|
userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
userGroupProjectMembershipRoleIsTemporary,
|
||||||
|
userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryRange
|
||||||
|
}) => ({
|
||||||
|
id: userGroupProjectMembershipRoleId,
|
||||||
|
role: userGroupProjectMembershipRole,
|
||||||
|
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userGroupProjectMembershipRolePermission,
|
||||||
|
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userGroupProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "userApId",
|
key: "userProjectMembershipRoleId",
|
||||||
|
label: "projecMembershiptRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
userProjectMembershipRoleId,
|
||||||
|
userProjectMembershipRole,
|
||||||
|
userProjectCustomRolePermission,
|
||||||
|
userProjectMembershipRoleIsTemporary,
|
||||||
|
userProjectMembershipRoleTemporaryMode,
|
||||||
|
userProjectMembershipRoleTemporaryRange,
|
||||||
|
userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userProjectMembershipRoleCustomRoleSlug
|
||||||
|
}) => ({
|
||||||
|
id: userProjectMembershipRoleId,
|
||||||
|
role: userProjectMembershipRole,
|
||||||
|
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userProjectCustomRolePermission,
|
||||||
|
temporaryRange: userProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userAdditionalPrivilegesId",
|
||||||
label: "additionalPrivileges" as const,
|
label: "additionalPrivileges" as const,
|
||||||
mapper: ({
|
mapper: ({
|
||||||
userApId,
|
userAdditionalPrivilegesId,
|
||||||
userApPermissions,
|
userAdditionalPrivilegesPermissions,
|
||||||
userApIsTemporary,
|
userAdditionalPrivilegesIsTemporary,
|
||||||
userApTemporaryMode,
|
userAdditionalPrivilegesTemporaryMode,
|
||||||
userApTemporaryRange,
|
userAdditionalPrivilegesTemporaryRange,
|
||||||
userApTemporaryAccessEndTime,
|
userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
userApTemporaryAccessStartTime
|
userAdditionalPrivilegesTemporaryAccessStartTime
|
||||||
}) => ({
|
}) => ({
|
||||||
id: userApId,
|
id: userAdditionalPrivilegesId,
|
||||||
permissions: userApPermissions,
|
permissions: userAdditionalPrivilegesPermissions,
|
||||||
temporaryRange: userApTemporaryRange,
|
temporaryRange: userAdditionalPrivilegesTemporaryRange,
|
||||||
temporaryMode: userApTemporaryMode,
|
temporaryMode: userAdditionalPrivilegesTemporaryMode,
|
||||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
|
||||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
isTemporary: userApIsTemporary
|
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupPermission = groupDocs.length
|
if (!userPermission) return undefined;
|
||||||
? sqlNestRelationships({
|
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined;
|
||||||
data: groupDocs,
|
|
||||||
key: "projectId",
|
|
||||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
|
||||||
orgId,
|
|
||||||
orgAuthEnforced,
|
|
||||||
userId,
|
|
||||||
id: membershipId,
|
|
||||||
projectId,
|
|
||||||
createdAt: membershipCreatedAt,
|
|
||||||
updatedAt: membershipUpdatedAt
|
|
||||||
}),
|
|
||||||
childrenMapper: [
|
|
||||||
{
|
|
||||||
key: "id",
|
|
||||||
label: "roles" as const,
|
|
||||||
mapper: (data) =>
|
|
||||||
ProjectUserMembershipRolesSchema.extend({
|
|
||||||
permissions: z.unknown(),
|
|
||||||
customRoleSlug: z.string().optional().nullable()
|
|
||||||
}).parse(data)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "userApId",
|
|
||||||
label: "additionalPrivileges" as const,
|
|
||||||
mapper: ({
|
|
||||||
userApId,
|
|
||||||
userApProjectId,
|
|
||||||
userApUserId,
|
|
||||||
userApPermissions,
|
|
||||||
userApIsTemporary,
|
|
||||||
userApTemporaryMode,
|
|
||||||
userApTemporaryRange,
|
|
||||||
userApTemporaryAccessEndTime,
|
|
||||||
userApTemporaryAccessStartTime
|
|
||||||
}) => ({
|
|
||||||
id: userApId,
|
|
||||||
userId: userApUserId,
|
|
||||||
projectId: userApProjectId,
|
|
||||||
permissions: userApPermissions,
|
|
||||||
temporaryRange: userApTemporaryRange,
|
|
||||||
temporaryMode: userApTemporaryMode,
|
|
||||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
|
||||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
|
||||||
isTemporary: userApIsTemporary
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!permission?.[0] && !groupPermission[0]) return undefined;
|
|
||||||
|
|
||||||
// when introducting cron mode change it here
|
// when introducting cron mode change it here
|
||||||
const activeRoles =
|
const activeRoles =
|
||||||
permission?.[0]?.roles?.filter(
|
userPermission?.projecMembershiptRoles?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeGroupRoles =
|
const activeGroupRoles =
|
||||||
groupPermission?.[0]?.roles?.filter(
|
userPermission?.userGroupRoles?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeAdditionalPrivileges =
|
const activeAdditionalPrivileges =
|
||||||
permission?.[0]?.additionalPrivileges?.filter(
|
userPermission?.additionalPrivileges?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeGroupAdditionalPrivileges =
|
|
||||||
groupPermission?.[0]?.additionalPrivileges?.filter(
|
|
||||||
({ isTemporary, temporaryAccessEndTime, userId: apUserId, projectId: apProjectId }) =>
|
|
||||||
apProjectId === projectId &&
|
|
||||||
apUserId === userId &&
|
|
||||||
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(permission[0] || groupPermission[0]),
|
...userPermission,
|
||||||
roles: [...activeRoles, ...activeGroupRoles],
|
roles: [...activeRoles, ...activeGroupRoles],
|
||||||
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
|
additionalPrivileges: activeAdditionalPrivileges
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
|
@ -20,7 +20,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
import { validateOrgSAML } from "./permission-fns";
|
import { validateOrgSAML } from "./permission-fns";
|
||||||
import { TBuildProjectPermissionDTO } from "./permission-types";
|
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
|
||||||
import {
|
import {
|
||||||
buildServiceTokenProjectPermission,
|
buildServiceTokenProjectPermission,
|
||||||
projectAdminPermissions,
|
projectAdminPermissions,
|
||||||
@ -47,7 +47,9 @@ export const permissionServiceFactory = ({
|
|||||||
serviceTokenDAL,
|
serviceTokenDAL,
|
||||||
projectDAL
|
projectDAL
|
||||||
}: TPermissionServiceFactoryDep) => {
|
}: TPermissionServiceFactoryDep) => {
|
||||||
const buildOrgPermission = (role: string, permission?: unknown) => {
|
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
|
||||||
|
const rules = orgUserRoles
|
||||||
|
.map(({ role, permissions }) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case OrgMembershipRole.Admin:
|
case OrgMembershipRole.Admin:
|
||||||
return orgAdminPermissions;
|
return orgAdminPermissions;
|
||||||
@ -56,17 +58,18 @@ export const permissionServiceFactory = ({
|
|||||||
case OrgMembershipRole.NoAccess:
|
case OrgMembershipRole.NoAccess:
|
||||||
return orgNoAccessPermissions;
|
return orgNoAccessPermissions;
|
||||||
case OrgMembershipRole.Custom:
|
case OrgMembershipRole.Custom:
|
||||||
return createMongoAbility<OrgPermissionSet>(
|
return unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
|
||||||
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
|
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
|
||||||
permission as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
|
|
||||||
),
|
|
||||||
{
|
|
||||||
conditionsMatcher
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
|
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.reduce((curr, prev) => prev.concat(curr), []);
|
||||||
|
|
||||||
|
return createMongoAbility<OrgPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||||
@ -129,7 +132,13 @@ export const permissionServiceFactory = ({
|
|||||||
|
|
||||||
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
||||||
|
|
||||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
||||||
|
membership?.groups?.map(({ role, customRolePermission }) => ({
|
||||||
|
role,
|
||||||
|
permissions: customRolePermission
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
return { permission: buildOrgPermission(finalPolicyRoles), membership };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
|
const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
|
||||||
@ -138,7 +147,10 @@ export const permissionServiceFactory = ({
|
|||||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||||
throw new BadRequestError({ name: "Custom permission not found" });
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
}
|
}
|
||||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
return {
|
||||||
|
permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]),
|
||||||
|
membership
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOrgPermission = async (
|
const getOrgPermission = async (
|
||||||
@ -169,11 +181,11 @@ export const permissionServiceFactory = ({
|
|||||||
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||||
if (!orgRole) throw new BadRequestError({ message: "Role not found" });
|
if (!orgRole) throw new BadRequestError({ message: "Role not found" });
|
||||||
return {
|
return {
|
||||||
permission: buildOrgPermission(OrgMembershipRole.Custom, orgRole.permissions),
|
permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
|
||||||
role: orgRole
|
role: orgRole
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { permission: buildOrgPermission(role, []) };
|
return { permission: buildOrgPermission([{ role, permissions: [] }]) };
|
||||||
};
|
};
|
||||||
|
|
||||||
// user permission for a project in an organization
|
// user permission for a project in an organization
|
||||||
@ -334,7 +346,7 @@ export const permissionServiceFactory = ({
|
|||||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||||
if (isCustomRole) {
|
if (isCustomRole) {
|
||||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
|
if (!projectRole) throw new BadRequestError({ message: `Role not found: ${role}` });
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission([
|
permission: buildProjectPermission([
|
||||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||||
|
@ -2,3 +2,8 @@ export type TBuildProjectPermissionDTO = {
|
|||||||
permissions?: unknown;
|
permissions?: unknown;
|
||||||
role: string;
|
role: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
export type TBuildOrgPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
|
@ -145,6 +145,8 @@ export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermiss
|
|||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
||||||
|
|
||||||
|
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
|
||||||
|
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions.
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas";
|
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies, TUsers } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||||
|
|
||||||
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
|
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
|
||||||
|
|
||||||
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||||
@ -20,14 +22,29 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||||
|
"secretApprovalPolicyApproverUser.id"
|
||||||
|
)
|
||||||
|
.leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.select(
|
.select(
|
||||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"),
|
||||||
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
|
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
||||||
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
|
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName")
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
tx.ref("approverGroupId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
|
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||||
|
tx.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
|
||||||
|
tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"),
|
||||||
|
tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName")
|
||||||
)
|
)
|
||||||
.select(
|
.select(
|
||||||
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
@ -55,11 +72,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "userApprovers" as const,
|
label: "userApprovers" as const,
|
||||||
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
|
mapper: ({
|
||||||
userId: approverUserId,
|
approverUserId: userId,
|
||||||
email: approverEmail,
|
approverEmail: email,
|
||||||
firstName: approverFirstName,
|
approverFirstName: firstName,
|
||||||
lastName: approverLastName
|
approverLastName: lastName
|
||||||
|
}) => ({
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupUserId",
|
||||||
|
label: "userApprovers" as const,
|
||||||
|
mapper: ({
|
||||||
|
approverGroupUserId: userId,
|
||||||
|
approverGroupEmail: email,
|
||||||
|
approverGroupFirstName: firstName,
|
||||||
|
approverGroupLastName: lastName
|
||||||
|
}) => ({
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -83,11 +120,34 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
...SecretApprovalPoliciesSchema.parse(data)
|
...SecretApprovalPoliciesSchema.parse(data)
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "approverUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverUserId: id }) => ({
|
||||||
|
type: ApproverType.User,
|
||||||
|
id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupId: id }) => ({
|
||||||
|
type: ApproverType.Group,
|
||||||
|
id
|
||||||
|
})
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "userApprovers" as const,
|
label: "userApprovers" as const,
|
||||||
mapper: ({ approverUserId }) => ({
|
mapper: ({ approverUserId: userId }) => ({
|
||||||
userId: approverUserId
|
userId
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupUserId",
|
||||||
|
label: "userApprovers" as const,
|
||||||
|
mapper: ({ approverGroupUserId: userId }) => ({
|
||||||
|
userId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@app/lib/fn";
|
|||||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
|
|
||||||
|
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||||
@ -54,7 +55,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
environment,
|
environment,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
}: TCreateSapDTO) => {
|
}: TCreateSapDTO) => {
|
||||||
if (approvals > approvers.length)
|
const groupApprovers = approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
const userApprovers = approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.User)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
|
||||||
|
if (!groupApprovers && approvals > approvers.length)
|
||||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -91,13 +99,22 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
await secretApprovalPolicyApproverDAL.insertMany(
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
approvers.map((approverUserId) => ({
|
userApprovers.map((approverUserId) => ({
|
||||||
approverUserId,
|
approverUserId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
|
groupApprovers.map((approverGroupId) => ({
|
||||||
|
approverGroupId,
|
||||||
|
policyId: doc.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return { ...secretApproval, environment: env, projectId };
|
return { ...secretApproval, environment: env, projectId };
|
||||||
@ -115,6 +132,13 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
secretPolicyId,
|
secretPolicyId,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
}: TUpdateSapDTO) => {
|
}: TUpdateSapDTO) => {
|
||||||
|
const groupApprovers = approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
const userApprovers = approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.User)
|
||||||
|
.map((approver) => approver.id);
|
||||||
|
|
||||||
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||||
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||||
|
|
||||||
@ -146,16 +170,28 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
if (approvers) {
|
|
||||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||||
|
|
||||||
|
if (approvers) {
|
||||||
await secretApprovalPolicyApproverDAL.insertMany(
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
approvers.map((approverUserId) => ({
|
userApprovers.map((approverUserId) => ({
|
||||||
approverUserId,
|
approverUserId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (groupApprovers) {
|
||||||
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
|
groupApprovers.map((approverGroupId) => ({
|
||||||
|
approverGroupId,
|
||||||
|
policyId: doc.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
|
import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||||
|
|
||||||
export type TCreateSapDTO = {
|
export type TCreateSapDTO = {
|
||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: string[];
|
approvers: { type: ApproverType; id: string }[];
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@ -14,7 +16,7 @@ export type TUpdateSapDTO = {
|
|||||||
secretPolicyId: string;
|
secretPolicyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
approvers: string[];
|
approvers: { type: ApproverType; id: string }[];
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@ -48,16 +48,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||||
`committerUser.id`
|
`committerUser.id`
|
||||||
)
|
)
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.SecretApprovalPolicyApprover,
|
TableName.SecretApprovalPolicyApprover,
|
||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
.join<TUsers>(
|
.leftJoin<TUsers>(
|
||||||
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
||||||
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||||
"secretApprovalPolicyApproverUser.id"
|
"secretApprovalPolicyApproverUser.id"
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"),
|
||||||
|
`${TableName.UserGroupMembership}.userId`,
|
||||||
|
`secretApprovalPolicyGroupApproverUser.id`
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretApprovalRequestReviewer,
|
TableName.SecretApprovalRequestReviewer,
|
||||||
`${TableName.SecretApprovalRequest}.id`,
|
`${TableName.SecretApprovalRequest}.id`,
|
||||||
@ -71,10 +81,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
|
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
|
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||||
|
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||||
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
||||||
|
tx.ref("firstName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
|
||||||
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
|
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
|
||||||
|
tx.ref("lastName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
|
||||||
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
|
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
|
||||||
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
|
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
|
||||||
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
|
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
|
||||||
@ -152,13 +167,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({
|
mapper: ({
|
||||||
approverUserId,
|
approverUserId: userId,
|
||||||
approverEmail: email,
|
approverEmail: email,
|
||||||
approverUsername: username,
|
approverUsername: username,
|
||||||
approverLastName: lastName,
|
approverLastName: lastName,
|
||||||
approverFirstName: firstName
|
approverFirstName: firstName
|
||||||
}) => ({
|
}) => ({
|
||||||
userId: approverUserId,
|
userId,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
username
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({
|
||||||
|
approverGroupUserId: userId,
|
||||||
|
approverGroupEmail: email,
|
||||||
|
approverGroupUsername: username,
|
||||||
|
approverGroupLastName: lastName,
|
||||||
|
approverGroupFirstName: firstName
|
||||||
|
}) => ({
|
||||||
|
userId,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
@ -236,11 +268,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalRequest}.policyId`,
|
`${TableName.SecretApprovalRequest}.policyId`,
|
||||||
`${TableName.SecretApprovalPolicy}.id`
|
`${TableName.SecretApprovalPolicy}.id`
|
||||||
)
|
)
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.SecretApprovalPolicyApprover,
|
TableName.SecretApprovalPolicyApprover,
|
||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
.join<TUsers>(
|
.join<TUsers>(
|
||||||
db(TableName.Users).as("committerUser"),
|
db(TableName.Users).as("committerUser"),
|
||||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||||
@ -269,6 +306,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
void bd
|
void bd
|
||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||||
|
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
|
||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
@ -289,6 +327,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
|
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||||
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
@ -334,7 +373,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId }) => approverUserId
|
mapper: ({ approverUserId }) => ({ userId: approverUserId })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "commitId",
|
key: "commitId",
|
||||||
@ -344,6 +383,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
id,
|
id,
|
||||||
secretId
|
secretId
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId })
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -371,11 +415,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalRequest}.policyId`,
|
`${TableName.SecretApprovalRequest}.policyId`,
|
||||||
`${TableName.SecretApprovalPolicy}.id`
|
`${TableName.SecretApprovalPolicy}.id`
|
||||||
)
|
)
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.SecretApprovalPolicyApprover,
|
TableName.SecretApprovalPolicyApprover,
|
||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
.join<TUsers>(
|
.join<TUsers>(
|
||||||
db(TableName.Users).as("committerUser"),
|
db(TableName.Users).as("committerUser"),
|
||||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||||
@ -404,6 +453,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
void bd
|
void bd
|
||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||||
|
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
|
||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
@ -424,6 +474,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
|
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||||
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
@ -469,7 +520,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId }) => approverUserId
|
mapper: ({ approverUserId }) => ({ userId: approverUserId })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "commitId",
|
key: "commitId",
|
||||||
@ -479,6 +530,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
id,
|
id,
|
||||||
secretId
|
secretId
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupUserId }) => ({
|
||||||
|
userId: approverGroupUserId
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -447,8 +447,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
const hasMinApproval =
|
const hasMinApproval =
|
||||||
secretApprovalRequest.policy.approvals <=
|
secretApprovalRequest.policy.approvals <=
|
||||||
secretApprovalRequest.policy.approvers.filter(
|
secretApprovalRequest.policy.approvers.filter(({ userId: approverId }) =>
|
||||||
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false
|
||||||
).length;
|
).length;
|
||||||
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
|
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||||
|
|
||||||
@ -805,7 +805,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||||
const approverUsers = await userDAL.find({
|
const approverUsers = await userDAL.find({
|
||||||
$in: {
|
$in: {
|
||||||
id: policy.approvers.map((approver: { userId: string }) => approver.userId)
|
id: policy.approvers.map((approver: { userId: string | null | undefined }) => approver.userId!)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,26 +5,30 @@ export const GROUPS = {
|
|||||||
role: "The role of the group to create."
|
role: "The role of the group to create."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
currentSlug: "The current slug of the group to update.",
|
id: "The id of the group to update",
|
||||||
name: "The new name of the group to update to.",
|
name: "The new name of the group to update to.",
|
||||||
slug: "The new slug of the group to update to.",
|
slug: "The new slug of the group to update to.",
|
||||||
role: "The new role of the group to update to."
|
role: "The new role of the group to update to."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
|
id: "The id of the group to delete",
|
||||||
slug: "The slug of the group to delete"
|
slug: "The slug of the group to delete"
|
||||||
},
|
},
|
||||||
LIST_USERS: {
|
LIST_USERS: {
|
||||||
slug: "The slug of the group to list users for",
|
id: "The id of the group to list users for",
|
||||||
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
|
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
|
||||||
limit: "The number of users to return.",
|
limit: "The number of users to return.",
|
||||||
username: "The username to search for."
|
username: "The username to search for."
|
||||||
},
|
},
|
||||||
ADD_USER: {
|
ADD_USER: {
|
||||||
slug: "The slug of the group to add the user to.",
|
id: "The id of the group to add the user to.",
|
||||||
username: "The username of the user to add to the group."
|
username: "The username of the user to add to the group."
|
||||||
},
|
},
|
||||||
|
GET_BY_ID: {
|
||||||
|
id: "The id of the group to fetch"
|
||||||
|
},
|
||||||
DELETE_USER: {
|
DELETE_USER: {
|
||||||
slug: "The slug of the group to remove the user from.",
|
id: "The id of the group to remove the user from.",
|
||||||
username: "The username of the user to remove from the group."
|
username: "The username of the user to remove from the group."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
@ -409,21 +413,21 @@ export const PROJECTS = {
|
|||||||
secretSnapshotId: "The ID of the snapshot to rollback to."
|
secretSnapshotId: "The ID of the snapshot to rollback to."
|
||||||
},
|
},
|
||||||
ADD_GROUP_TO_PROJECT: {
|
ADD_GROUP_TO_PROJECT: {
|
||||||
projectSlug: "The slug of the project to add the group to.",
|
projectId: "The ID of the project to add the group to.",
|
||||||
groupSlug: "The slug of the group to add to the project.",
|
groupId: "The ID of the group to add to the project.",
|
||||||
role: "The role for the group to assume in the project."
|
role: "The role for the group to assume in the project."
|
||||||
},
|
},
|
||||||
UPDATE_GROUP_IN_PROJECT: {
|
UPDATE_GROUP_IN_PROJECT: {
|
||||||
projectSlug: "The slug of the project to update the group in.",
|
projectId: "The ID of the project to update the group in.",
|
||||||
groupSlug: "The slug of the group to update in the project.",
|
groupId: "The ID of the group to update in the project.",
|
||||||
roles: "A list of roles to update the group to."
|
roles: "A list of roles to update the group to."
|
||||||
},
|
},
|
||||||
REMOVE_GROUP_FROM_PROJECT: {
|
REMOVE_GROUP_FROM_PROJECT: {
|
||||||
projectSlug: "The slug of the project to delete the group from.",
|
projectId: "The ID of the project to delete the group from.",
|
||||||
groupSlug: "The slug of the group to delete from the project."
|
groupId: "The ID of the group to delete from the project."
|
||||||
},
|
},
|
||||||
LIST_GROUPS_IN_PROJECT: {
|
LIST_GROUPS_IN_PROJECT: {
|
||||||
projectSlug: "The slug of the project to list groups for."
|
projectId: "The ID of the project to list groups for."
|
||||||
},
|
},
|
||||||
LIST_INTEGRATION: {
|
LIST_INTEGRATION: {
|
||||||
workspaceId: "The ID of the project to list integrations for."
|
workspaceId: "The ID of the project to list integrations for."
|
||||||
@ -697,11 +701,46 @@ export const SECRET_IMPORTS = {
|
|||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const DASHBOARD = {
|
||||||
|
SECRET_OVERVIEW_LIST: {
|
||||||
|
projectId: "The ID of the project to list secrets/folders from.",
|
||||||
|
environments:
|
||||||
|
"The slugs of the environments to list secrets/folders from (comma separated, ie 'environments=dev,staging,prod').",
|
||||||
|
secretPath: "The secret path to list secrets/folders from.",
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
|
||||||
|
limit: "The number of secrets/folders to return.",
|
||||||
|
orderBy: "The column to order secrets/folders by.",
|
||||||
|
orderDirection: "The direction to order secrets/folders in.",
|
||||||
|
search: "The text string to filter secret keys and folder names by.",
|
||||||
|
includeSecrets: "Whether to include project secrets in the response.",
|
||||||
|
includeFolders: "Whether to include project folders in the response.",
|
||||||
|
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||||
|
},
|
||||||
|
SECRET_DETAILS_LIST: {
|
||||||
|
projectId: "The ID of the project to list secrets/folders from.",
|
||||||
|
environment: "The slug of the environment to list secrets/folders from.",
|
||||||
|
secretPath: "The secret path to list secrets/folders from.",
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
|
||||||
|
limit: "The number of secrets/folders to return.",
|
||||||
|
orderBy: "The column to order secrets/folders by.",
|
||||||
|
orderDirection: "The direction to order secrets/folders in.",
|
||||||
|
search: "The text string to filter secret keys and folder names by.",
|
||||||
|
tags: "The tags to filter secrets by (comma separated, ie 'tags=billing,engineering').",
|
||||||
|
includeSecrets: "Whether to include project secrets in the response.",
|
||||||
|
includeFolders: "Whether to include project folders in the response.",
|
||||||
|
includeImports: "Whether to include project secret imports in the response.",
|
||||||
|
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const AUDIT_LOGS = {
|
export const AUDIT_LOGS = {
|
||||||
EXPORT: {
|
EXPORT: {
|
||||||
workspaceId: "The ID of the project to export audit logs from.",
|
projectId:
|
||||||
|
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
||||||
eventType: "The type of the event to export.",
|
eventType: "The type of the event to export.",
|
||||||
userAgentType: "Choose which consuming application to export audit logs for.",
|
userAgentType: "Choose which consuming application to export audit logs for.",
|
||||||
|
eventMetadata:
|
||||||
|
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
|
||||||
startDate: "The date to start the export from.",
|
startDate: "The date to start the export from.",
|
||||||
endDate: "The date to end the export at.",
|
endDate: "The date to end the export at.",
|
||||||
offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
|
offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
|
||||||
|
@ -9,3 +9,8 @@ export const removeTrailingSlash = (str: string) => {
|
|||||||
|
|
||||||
return str.endsWith("/") ? str.slice(0, -1) : str;
|
return str.endsWith("/") ? str.slice(0, -1) : str;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const prefixWithSlash = (str: string) => {
|
||||||
|
if (str.startsWith("/")) return str;
|
||||||
|
return `/${str}`;
|
||||||
|
};
|
||||||
|
@ -51,11 +51,17 @@ export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean
|
|||||||
: unknown)
|
: unknown)
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
|
export type TFindOpt<
|
||||||
|
R extends object = object,
|
||||||
|
TCount extends boolean = boolean,
|
||||||
|
TCountDistinct extends keyof R | undefined = undefined
|
||||||
|
> = {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
||||||
|
groupBy?: keyof R;
|
||||||
count?: TCount;
|
count?: TCount;
|
||||||
|
countDistinct?: TCountDistinct;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,13 +92,18 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
|||||||
throw new DatabaseError({ error, name: "Find one" });
|
throw new DatabaseError({ error, name: "Find one" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
find: async <TCount extends boolean = false>(
|
find: async <
|
||||||
|
TCount extends boolean = false,
|
||||||
|
TCountDistinct extends keyof Tables[Tname]["base"] | undefined = undefined
|
||||||
|
>(
|
||||||
filter: TFindFilter<Tables[Tname]["base"]>,
|
filter: TFindFilter<Tables[Tname]["base"]>,
|
||||||
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
|
{ offset, limit, sort, count, tx, countDistinct }: TFindOpt<Tables[Tname]["base"], TCount, TCountDistinct> = {}
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
|
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
|
||||||
if (count) {
|
if (countDistinct) {
|
||||||
|
void query.countDistinct(countDistinct);
|
||||||
|
} else if (count) {
|
||||||
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
||||||
void query.select("*");
|
void query.select("*");
|
||||||
}
|
}
|
||||||
@ -101,7 +112,8 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
|||||||
if (sort) {
|
if (sort) {
|
||||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
}
|
}
|
||||||
const res = (await query) as TFindReturn<typeof query, TCount>;
|
|
||||||
|
const res = (await query) as TFindReturn<typeof query, TCountDistinct extends undefined ? TCount : true>;
|
||||||
return res;
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find one" });
|
throw new DatabaseError({ error, name: "Find one" });
|
||||||
|
@ -7,7 +7,11 @@ import {
|
|||||||
TScanFullRepoEventPayload,
|
TScanFullRepoEventPayload,
|
||||||
TScanPushEventPayload
|
TScanPushEventPayload
|
||||||
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||||
import { TSyncSecretsDTO } from "@app/services/secret/secret-types";
|
import {
|
||||||
|
TFailedIntegrationSyncEmailsPayload,
|
||||||
|
TIntegrationSyncPayload,
|
||||||
|
TSyncSecretsDTO
|
||||||
|
} from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
export enum QueueName {
|
export enum QueueName {
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
@ -42,6 +46,7 @@ export enum QueueJobs {
|
|||||||
SecWebhook = "secret-webhook-trigger",
|
SecWebhook = "secret-webhook-trigger",
|
||||||
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
||||||
IntegrationSync = "secret-integration-pull",
|
IntegrationSync = "secret-integration-pull",
|
||||||
|
SendFailedIntegrationSyncEmails = "send-failed-integration-sync-emails",
|
||||||
SecretScan = "secret-scan",
|
SecretScan = "secret-scan",
|
||||||
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
|
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
|
||||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||||
@ -88,17 +93,25 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.SecWebhook;
|
name: QueueJobs.SecWebhook;
|
||||||
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||||
};
|
};
|
||||||
[QueueName.IntegrationSync]: {
|
|
||||||
name: QueueJobs.IntegrationSync;
|
[QueueName.AccessTokenStatusUpdate]:
|
||||||
payload: {
|
| {
|
||||||
isManual?: boolean;
|
name: QueueJobs.IdentityAccessTokenStatusUpdate;
|
||||||
actorId?: string;
|
payload: { identityAccessTokenId: string; numberOfUses: number };
|
||||||
projectId: string;
|
}
|
||||||
environment: string;
|
| {
|
||||||
secretPath: string;
|
name: QueueJobs.ServiceTokenStatusUpdate;
|
||||||
depth?: number;
|
payload: { serviceTokenId: string };
|
||||||
deDupeQueue?: Record<string, boolean>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[QueueName.IntegrationSync]:
|
||||||
|
| {
|
||||||
|
name: QueueJobs.IntegrationSync;
|
||||||
|
payload: TIntegrationSyncPayload;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.SendFailedIntegrationSyncEmails;
|
||||||
|
payload: TFailedIntegrationSyncEmailsPayload;
|
||||||
};
|
};
|
||||||
[QueueName.SecretFullRepoScan]: {
|
[QueueName.SecretFullRepoScan]: {
|
||||||
name: QueueJobs.SecretScan;
|
name: QueueJobs.SecretScan;
|
||||||
@ -153,15 +166,6 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.ProjectV3Migration;
|
name: QueueJobs.ProjectV3Migration;
|
||||||
payload: { projectId: string };
|
payload: { projectId: string };
|
||||||
};
|
};
|
||||||
[QueueName.AccessTokenStatusUpdate]:
|
|
||||||
| {
|
|
||||||
name: QueueJobs.IdentityAccessTokenStatusUpdate;
|
|
||||||
payload: { identityAccessTokenId: string; numberOfUses: number };
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: QueueJobs.ServiceTokenStatusUpdate;
|
|
||||||
payload: { serviceTokenId: string };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -11,6 +12,12 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
|
|
||||||
|
enum JWTErrors {
|
||||||
|
JwtExpired = "jwt expired",
|
||||||
|
JwtMalformed = "jwt malformed",
|
||||||
|
InvalidAlgorithm = "invalid algorithm"
|
||||||
|
}
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
@ -36,6 +43,27 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
status: error.status,
|
status: error.status,
|
||||||
detail: error.detail
|
detail: error.detail
|
||||||
});
|
});
|
||||||
|
// Handle JWT errors and make them more human-readable for the end-user.
|
||||||
|
} else if (error instanceof jwt.JsonWebTokenError) {
|
||||||
|
const message = (() => {
|
||||||
|
if (error.message === JWTErrors.JwtExpired) {
|
||||||
|
return "Your token has expired. Please re-authenticate.";
|
||||||
|
}
|
||||||
|
if (error.message === JWTErrors.JwtMalformed) {
|
||||||
|
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
|
||||||
|
}
|
||||||
|
if (error.message === JWTErrors.InvalidAlgorithm) {
|
||||||
|
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.message;
|
||||||
|
})();
|
||||||
|
|
||||||
|
void res.status(401).send({
|
||||||
|
statusCode: 401,
|
||||||
|
error: "TokenError",
|
||||||
|
message
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
void res.send(error);
|
void res.send(error);
|
||||||
}
|
}
|
||||||
|
@ -923,6 +923,7 @@ export const registerRoutes = async (
|
|||||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||||
accessApprovalPolicyDAL,
|
accessApprovalPolicyDAL,
|
||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
|
groupDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
@ -942,7 +943,8 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
groupDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretReplicationService = secretReplicationServiceFactory({
|
const secretReplicationService = secretReplicationServiceFactory({
|
||||||
|
@ -74,7 +74,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
description: "Get all audit logs for an organization",
|
description: "Get all audit logs for an organization",
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string().optional(),
|
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
||||||
actorType: z.nativeEnum(ActorType).optional(),
|
actorType: z.nativeEnum(ActorType).optional(),
|
||||||
// eventType is split with , for multiple values, we need to transform it to array
|
// eventType is split with , for multiple values, we need to transform it to array
|
||||||
eventType: z
|
eventType: z
|
||||||
@ -102,7 +102,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
{} as Record<string, string>
|
{} as Record<string, string>
|
||||||
);
|
);
|
||||||
}),
|
})
|
||||||
|
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
|
||||||
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||||
@ -120,10 +121,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
project: z.object({
|
project: z
|
||||||
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string()
|
slug: z.string()
|
||||||
}),
|
})
|
||||||
|
.optional(),
|
||||||
event: z.object({
|
event: z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
metadata: z.any()
|
metadata: z.any()
|
||||||
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
|||||||
import { SecretFoldersSchema } from "@app/db/schemas";
|
import { SecretFoldersSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { FOLDERS } from "@app/lib/api-docs";
|
import { FOLDERS } from "@app/lib/api-docs";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -26,9 +26,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId),
|
workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId),
|
||||||
environment: z.string().trim().describe(FOLDERS.CREATE.environment),
|
environment: z.string().trim().describe(FOLDERS.CREATE.environment),
|
||||||
name: z.string().trim().describe(FOLDERS.CREATE.name),
|
name: z.string().trim().describe(FOLDERS.CREATE.name),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.path),
|
path: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.CREATE.path),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.directory)
|
directory: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.CREATE.directory)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -86,9 +98,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
|
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
|
||||||
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
|
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
|
||||||
name: z.string().trim().describe(FOLDERS.UPDATE.name),
|
name: z.string().trim().describe(FOLDERS.UPDATE.name),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path),
|
path: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.UPDATE.path),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.directory)
|
directory: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.UPDATE.directory)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -147,7 +171,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
id: z.string().describe(FOLDERS.UPDATE.folderId),
|
id: z.string().describe(FOLDERS.UPDATE.folderId),
|
||||||
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
|
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
|
||||||
name: z.string().trim().describe(FOLDERS.UPDATE.name),
|
name: z.string().trim().describe(FOLDERS.UPDATE.name),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path)
|
path: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.UPDATE.path)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1)
|
||||||
@ -211,9 +241,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId),
|
workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId),
|
||||||
environment: z.string().trim().describe(FOLDERS.DELETE.environment),
|
environment: z.string().trim().describe(FOLDERS.DELETE.environment),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.path),
|
path: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.DELETE.path),
|
||||||
// keep this here as cli need directory
|
// keep this here as cli need directory
|
||||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.directory)
|
directory: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.DELETE.directory)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -267,9 +309,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId),
|
workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId),
|
||||||
environment: z.string().trim().describe(FOLDERS.LIST.environment),
|
environment: z.string().trim().describe(FOLDERS.LIST.environment),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.path),
|
path: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.LIST.path),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.directory)
|
directory: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(prefixWithSlash)
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(FOLDERS.LIST.directory)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
ProjectUserMembershipRolesSchema
|
ProjectUserMembershipRolesSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||||
@ -15,8 +16,11 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member
|
|||||||
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:projectSlug/groups/:groupSlug",
|
url: "/:projectId/groups/:groupId",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Add group to project",
|
description: "Add group to project",
|
||||||
security: [
|
security: [
|
||||||
@ -25,16 +29,38 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug),
|
projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId),
|
||||||
groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug)
|
groupId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
|
.object({
|
||||||
role: z
|
role: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.default(ProjectMembershipRole.NoAccess)
|
.default(ProjectMembershipRole.NoAccess)
|
||||||
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role)
|
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role),
|
||||||
|
roles: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.object({
|
||||||
|
role: z.string(),
|
||||||
|
isTemporary: z.literal(false).default(false)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
role: z.string(),
|
||||||
|
isTemporary: z.literal(true),
|
||||||
|
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
|
||||||
|
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
|
||||||
|
temporaryAccessStartTime: z.string().datetime()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.refine((data) => data.role || data.roles, {
|
||||||
|
message: "Either role or roles must be present",
|
||||||
|
path: ["role", "roles"]
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -48,17 +74,18 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
groupSlug: req.params.groupSlug,
|
roles: req.body.roles || [{ role: req.body.role }],
|
||||||
projectSlug: req.params.projectSlug,
|
projectId: req.params.projectId,
|
||||||
role: req.body.role
|
groupId: req.params.groupId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { groupMembership };
|
return { groupMembership };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:projectSlug/groups/:groupSlug",
|
url: "/:projectId/groups/:groupId",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
description: "Update group in project",
|
description: "Update group in project",
|
||||||
@ -68,8 +95,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectSlug),
|
projectId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectId),
|
||||||
groupSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupSlug)
|
groupId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
roles: z
|
roles: z
|
||||||
@ -103,18 +130,22 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
groupSlug: req.params.groupSlug,
|
projectId: req.params.projectId,
|
||||||
projectSlug: req.params.projectSlug,
|
groupId: req.params.groupId,
|
||||||
roles: req.body.roles
|
roles: req.body.roles
|
||||||
});
|
});
|
||||||
|
|
||||||
return { roles };
|
return { roles };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:projectSlug/groups/:groupSlug",
|
url: "/:projectId/groups/:groupId",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Remove group from project",
|
description: "Remove group from project",
|
||||||
security: [
|
security: [
|
||||||
@ -123,8 +154,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectSlug),
|
projectId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectId),
|
||||||
groupSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupSlug)
|
groupId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -138,17 +169,21 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
groupSlug: req.params.groupSlug,
|
groupId: req.params.groupId,
|
||||||
projectSlug: req.params.projectSlug
|
projectId: req.params.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { groupMembership };
|
return { groupMembership };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:projectSlug/groups",
|
url: "/:projectId/groups",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Return list of groups in project",
|
description: "Return list of groups in project",
|
||||||
security: [
|
security: [
|
||||||
@ -157,7 +192,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectSlug: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectSlug)
|
projectId: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -193,9 +228,67 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectSlug: req.params.projectSlug
|
projectId: req.params.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { groupMemberships };
|
return { groupMemberships };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/groups/:groupId",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Return project group",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
groupId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groupMembership: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
groupId: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
roles: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
customRoleId: z.string().optional().nullable(),
|
||||||
|
customRoleName: z.string().optional().nullable(),
|
||||||
|
customRoleSlug: z.string().optional().nullable(),
|
||||||
|
isTemporary: z.boolean(),
|
||||||
|
temporaryMode: z.string().optional().nullable(),
|
||||||
|
temporaryRange: z.string().nullable().optional(),
|
||||||
|
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||||
|
temporaryAccessEndTime: z.date().nullable().optional()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
group: GroupsSchema.pick({ name: true, id: true, slug: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const groupMembership = await server.services.groupProject.getGroupInProject({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.params
|
||||||
|
});
|
||||||
|
|
||||||
|
return { groupMembership };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
612
backend/src/server/routes/v3/dashboard-router.ts
Normal file
612
backend/src/server/routes/v3/dashboard-router.ts
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { DASHBOARD } from "@app/lib/api-docs";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { secretsLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
|
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/secrets-overview",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List project secrets overview",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.projectId),
|
||||||
|
environments: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.transform(decodeURIComponent)
|
||||||
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.environments),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.secretPath),
|
||||||
|
offset: z.coerce.number().min(0).optional().default(0).describe(DASHBOARD.SECRET_OVERVIEW_LIST.offset),
|
||||||
|
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(DASHBOARD.SECRET_OVERVIEW_LIST.limit),
|
||||||
|
orderBy: z
|
||||||
|
.nativeEnum(SecretsOrderBy)
|
||||||
|
.default(SecretsOrderBy.Name)
|
||||||
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderBy)
|
||||||
|
.optional(),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
|
||||||
|
.optional(),
|
||||||
|
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
|
||||||
|
includeSecrets: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||||
|
includeFolders: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||||
|
includeDynamicSecrets: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(),
|
||||||
|
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
|
||||||
|
secrets: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
color: true
|
||||||
|
})
|
||||||
|
.extend({ name: z.string() })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
totalFolderCount: z.number().optional(),
|
||||||
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
|
totalSecretCount: z.number().optional(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
secretPath,
|
||||||
|
projectId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
search,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
includeFolders,
|
||||||
|
includeSecrets,
|
||||||
|
includeDynamicSecrets
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
const environments = req.query.environments.split(",");
|
||||||
|
|
||||||
|
if (!projectId || environments.length === 0)
|
||||||
|
throw new BadRequestError({ message: "Missing workspace id or environment(s)" });
|
||||||
|
|
||||||
|
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
|
||||||
|
|
||||||
|
// prevent older projects from accessing endpoint
|
||||||
|
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
|
||||||
|
|
||||||
|
let remainingLimit = limit;
|
||||||
|
let adjustedOffset = offset;
|
||||||
|
|
||||||
|
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
|
||||||
|
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
|
||||||
|
let dynamicSecrets:
|
||||||
|
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByFolderIds>>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
let totalFolderCount: number | undefined;
|
||||||
|
let totalDynamicSecretCount: number | undefined;
|
||||||
|
let totalSecretCount: number | undefined;
|
||||||
|
|
||||||
|
if (includeFolders) {
|
||||||
|
// this is the unique count, ie duplicate folders across envs only count as 1
|
||||||
|
totalFolderCount = await server.services.folder.getProjectFolderCount({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
path: secretPath,
|
||||||
|
environments,
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalFolderCount > adjustedOffset) {
|
||||||
|
folders = await server.services.folder.getFoldersMultiEnv({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
environments,
|
||||||
|
path: secretPath,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the count of unique folder names to properly adjust remaining limit
|
||||||
|
const uniqueFolderCount = new Set(folders.map((folder) => folder.name)).size;
|
||||||
|
|
||||||
|
remainingLimit -= uniqueFolderCount;
|
||||||
|
adjustedOffset = 0;
|
||||||
|
} else {
|
||||||
|
adjustedOffset = Math.max(0, adjustedOffset - totalFolderCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDynamicSecrets) {
|
||||||
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
|
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
search,
|
||||||
|
environmentSlugs: environments,
|
||||||
|
path: secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
|
||||||
|
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
search,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
environmentSlugs: environments,
|
||||||
|
path: secretPath,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the count of unique dynamic secret names to properly adjust remaining limit
|
||||||
|
const uniqueDynamicSecretsCount = new Set(dynamicSecrets.map((dynamicSecret) => dynamicSecret.name)).size;
|
||||||
|
|
||||||
|
remainingLimit -= uniqueDynamicSecretsCount;
|
||||||
|
adjustedOffset = 0;
|
||||||
|
} else {
|
||||||
|
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeSecrets) {
|
||||||
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
|
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environments,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||||
|
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environments,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const environment of environments) {
|
||||||
|
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||||
|
|
||||||
|
if (secretCountFromEnv) {
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRETS,
|
||||||
|
metadata: {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
numberOfSecrets: secretCountFromEnv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretPulled,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secretCountFromEnv,
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
folders,
|
||||||
|
dynamicSecrets,
|
||||||
|
secrets,
|
||||||
|
totalFolderCount,
|
||||||
|
totalDynamicSecretCount,
|
||||||
|
totalSecretCount,
|
||||||
|
totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/secrets-details",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List project secrets details",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.projectId),
|
||||||
|
environment: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.environment),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(DASHBOARD.SECRET_DETAILS_LIST.secretPath),
|
||||||
|
offset: z.coerce.number().min(0).optional().default(0).describe(DASHBOARD.SECRET_DETAILS_LIST.offset),
|
||||||
|
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(DASHBOARD.SECRET_DETAILS_LIST.limit),
|
||||||
|
orderBy: z
|
||||||
|
.nativeEnum(SecretsOrderBy)
|
||||||
|
.default(SecretsOrderBy.Name)
|
||||||
|
.describe(DASHBOARD.SECRET_DETAILS_LIST.orderBy)
|
||||||
|
.optional(),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(DASHBOARD.SECRET_DETAILS_LIST.orderDirection)
|
||||||
|
.optional(),
|
||||||
|
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
||||||
|
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
||||||
|
includeSecrets: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||||
|
includeFolders: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||||
|
includeDynamicSecrets: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||||
|
includeImports: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
imports: SecretImportsSchema.omit({ importEnv: true })
|
||||||
|
.extend({
|
||||||
|
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
folders: SecretFoldersSchema.array().optional(),
|
||||||
|
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
|
||||||
|
secrets: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
color: true
|
||||||
|
})
|
||||||
|
.extend({ name: z.string() })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
totalImportCount: z.number().optional(),
|
||||||
|
totalFolderCount: z.number().optional(),
|
||||||
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
|
totalSecretCount: z.number().optional(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
search,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
includeFolders,
|
||||||
|
includeSecrets,
|
||||||
|
includeDynamicSecrets,
|
||||||
|
includeImports
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
|
||||||
|
|
||||||
|
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
|
||||||
|
|
||||||
|
// prevent older projects from accessing endpoint
|
||||||
|
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
|
||||||
|
|
||||||
|
const tags = req.query.tags?.split(",") ?? [];
|
||||||
|
|
||||||
|
let remainingLimit = limit;
|
||||||
|
let adjustedOffset = offset;
|
||||||
|
|
||||||
|
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined;
|
||||||
|
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
|
||||||
|
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"] | undefined;
|
||||||
|
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
|
||||||
|
|
||||||
|
let totalImportCount: number | undefined;
|
||||||
|
let totalFolderCount: number | undefined;
|
||||||
|
let totalDynamicSecretCount: number | undefined;
|
||||||
|
let totalSecretCount: number | undefined;
|
||||||
|
|
||||||
|
if (includeImports) {
|
||||||
|
totalImportCount = await server.services.secretImport.getProjectImportCount({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path: secretPath,
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalImportCount > adjustedOffset) {
|
||||||
|
imports = await server.services.secretImport.getImports({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path: secretPath,
|
||||||
|
search,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_IMPORTS,
|
||||||
|
metadata: {
|
||||||
|
environment,
|
||||||
|
folderId: imports?.[0]?.folderId,
|
||||||
|
numberOfImports: imports.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
remainingLimit -= imports.length;
|
||||||
|
adjustedOffset = 0;
|
||||||
|
} else {
|
||||||
|
adjustedOffset = Math.max(0, adjustedOffset - totalImportCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeFolders) {
|
||||||
|
totalFolderCount = await server.services.folder.getProjectFolderCount({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
environments: [environment],
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalFolderCount > adjustedOffset) {
|
||||||
|
folders = await server.services.folder.getFolders({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
path: secretPath,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset
|
||||||
|
});
|
||||||
|
|
||||||
|
remainingLimit -= folders.length;
|
||||||
|
adjustedOffset = 0;
|
||||||
|
} else {
|
||||||
|
adjustedOffset = Math.max(0, adjustedOffset - totalFolderCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDynamicSecrets) {
|
||||||
|
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
search,
|
||||||
|
environmentSlug: environment,
|
||||||
|
path: secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
|
||||||
|
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnv({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
search,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
environmentSlug: environment,
|
||||||
|
path: secretPath,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset
|
||||||
|
});
|
||||||
|
|
||||||
|
remainingLimit -= dynamicSecrets.length;
|
||||||
|
adjustedOffset = 0;
|
||||||
|
} else {
|
||||||
|
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeSecrets) {
|
||||||
|
totalSecretCount = await server.services.secret.getSecretsCount({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environment,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
search,
|
||||||
|
tagSlugs: tags
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||||
|
const secretsRaw = await server.services.secret.getSecretsRaw({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environment,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset,
|
||||||
|
tagSlugs: tags
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets = secretsRaw.secrets;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRETS,
|
||||||
|
metadata: {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
numberOfSecrets: secrets.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretPulled,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
imports,
|
||||||
|
folders,
|
||||||
|
dynamicSecrets,
|
||||||
|
secrets,
|
||||||
|
totalImportCount,
|
||||||
|
totalFolderCount,
|
||||||
|
totalDynamicSecretCount,
|
||||||
|
totalSecretCount,
|
||||||
|
totalCount:
|
||||||
|
(totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
|
import { registerDashboardRouter } from "./dashboard-router";
|
||||||
import { registerLoginRouter } from "./login-router";
|
import { registerLoginRouter } from "./login-router";
|
||||||
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
||||||
import { registerSecretRouter } from "./secret-router";
|
import { registerSecretRouter } from "./secret-router";
|
||||||
@ -10,4 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
||||||
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
};
|
};
|
||||||
|
@ -10,10 +10,15 @@ export type TGroupProjectDALFactory = ReturnType<typeof groupProjectDALFactory>;
|
|||||||
export const groupProjectDALFactory = (db: TDbClient) => {
|
export const groupProjectDALFactory = (db: TDbClient) => {
|
||||||
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
|
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
|
||||||
|
|
||||||
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
const findByProjectId = async (projectId: string, filter?: { groupId?: string }, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership)
|
const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership)
|
||||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.where((qb) => {
|
||||||
|
if (filter?.groupId) {
|
||||||
|
void qb.where(`${TableName.Groups}.id`, "=", filter.groupId);
|
||||||
|
}
|
||||||
|
})
|
||||||
.join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`)
|
.join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`)
|
||||||
.join(
|
.join(
|
||||||
TableName.GroupProjectMembershipRole,
|
TableName.GroupProjectMembershipRole,
|
||||||
|
@ -7,7 +7,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
|
|||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
||||||
@ -22,6 +22,7 @@ import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membershi
|
|||||||
import {
|
import {
|
||||||
TCreateProjectGroupDTO,
|
TCreateProjectGroupDTO,
|
||||||
TDeleteProjectGroupDTO,
|
TDeleteProjectGroupDTO,
|
||||||
|
TGetGroupInProjectDTO,
|
||||||
TListProjectGroupDTO,
|
TListProjectGroupDTO,
|
||||||
TUpdateProjectGroupDTO
|
TUpdateProjectGroupDTO
|
||||||
} from "./group-project-types";
|
} from "./group-project-types";
|
||||||
@ -33,7 +34,7 @@ type TGroupProjectServiceFactoryDep = {
|
|||||||
"create" | "transaction" | "insertMany" | "delete"
|
"create" | "transaction" | "insertMany" | "delete"
|
||||||
>;
|
>;
|
||||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembersNotInProject">;
|
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembersNotInProject">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser" | "findById">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
projectBotDAL: TProjectBotDALFactory;
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
@ -55,19 +56,17 @@ export const groupProjectServiceFactory = ({
|
|||||||
permissionService
|
permissionService
|
||||||
}: TGroupProjectServiceFactoryDep) => {
|
}: TGroupProjectServiceFactoryDep) => {
|
||||||
const addGroupToProject = async ({
|
const addGroupToProject = async ({
|
||||||
groupSlug,
|
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectSlug,
|
roles,
|
||||||
role
|
projectId,
|
||||||
|
groupId
|
||||||
}: TCreateProjectGroupDTO) => {
|
}: TCreateProjectGroupDTO) => {
|
||||||
const project = await projectDAL.findOne({
|
const project = await projectDAL.findById(projectId);
|
||||||
slug: projectSlug
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
if (!project) throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` });
|
||||||
if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
|
if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -79,25 +78,51 @@ export const groupProjectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
|
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
|
||||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
if (!group) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` });
|
||||||
|
|
||||||
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||||
if (existingGroup)
|
if (existingGroup)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Group with slug ${groupSlug} already exists in project with id ${project.id}`
|
message: `Group with ID ${groupId} already exists in project with id ${project.id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole(
|
for await (const { role: requestedRoleChange } of roles) {
|
||||||
role,
|
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
|
||||||
|
requestedRoleChange,
|
||||||
project.id
|
project.id
|
||||||
);
|
);
|
||||||
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
|
||||||
if (!hasPrivilege)
|
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "Failed to add group to project with more privileged role"
|
if (!hasRequiredPrivileges) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate custom roles input
|
||||||
|
const customInputRoles = roles.filter(
|
||||||
|
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
||||||
|
);
|
||||||
|
const hasCustomRole = Boolean(customInputRoles.length);
|
||||||
|
const customRoles = hasCustomRole
|
||||||
|
? await projectRoleDAL.find({
|
||||||
|
projectId: project.id,
|
||||||
|
$in: { slug: customInputRoles.map(({ role }) => role) }
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (customRoles.length !== customInputRoles.length) {
|
||||||
|
const customRoleSlugs = customRoles.map((customRole) => customRole.slug);
|
||||||
|
const missingInputRoles = customInputRoles
|
||||||
|
.filter((inputRole) => !customRoleSlugs.includes(inputRole.role))
|
||||||
|
.map((role) => role.role);
|
||||||
|
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Custom role/s not found: ${missingInputRoles.join(", ")}`
|
||||||
});
|
});
|
||||||
const isCustomRole = Boolean(customRole);
|
}
|
||||||
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
const projectGroup = await groupProjectDAL.transaction(async (tx) => {
|
const projectGroup = await groupProjectDAL.transaction(async (tx) => {
|
||||||
const groupProjectMembership = await groupProjectDAL.create(
|
const groupProjectMembership = await groupProjectDAL.create(
|
||||||
@ -108,14 +133,31 @@ export const groupProjectServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
await groupProjectMembershipRoleDAL.create(
|
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
|
||||||
{
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
|
||||||
|
if (!inputRole.isTemporary) {
|
||||||
|
return {
|
||||||
projectMembershipId: groupProjectMembership.id,
|
projectMembershipId: groupProjectMembership.id,
|
||||||
role: isCustomRole ? ProjectMembershipRole.Custom : role,
|
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
|
||||||
customRoleId: customRole?.id
|
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
|
||||||
},
|
};
|
||||||
tx
|
}
|
||||||
);
|
|
||||||
|
// check cron or relative here later for now its just relative
|
||||||
|
const relativeTimeInMs = ms(inputRole.temporaryRange);
|
||||||
|
return {
|
||||||
|
projectMembershipId: groupProjectMembership.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
|
||||||
|
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
|
||||||
|
temporaryRange: inputRole.temporaryRange,
|
||||||
|
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
|
||||||
|
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
||||||
|
|
||||||
// share project key with users in group that have not
|
// share project key with users in group that have not
|
||||||
// individually been added to the project and that are not part of
|
// individually been added to the project and that are not part of
|
||||||
@ -183,19 +225,17 @@ export const groupProjectServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateGroupInProject = async ({
|
const updateGroupInProject = async ({
|
||||||
projectSlug,
|
projectId,
|
||||||
groupSlug,
|
groupId,
|
||||||
roles,
|
roles,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TUpdateProjectGroupDTO) => {
|
}: TUpdateProjectGroupDTO) => {
|
||||||
const project = await projectDAL.findOne({
|
const project = await projectDAL.findById(projectId);
|
||||||
slug: projectSlug
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
if (!project) throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -206,11 +246,24 @@ export const groupProjectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
|
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
|
||||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
if (!group) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` });
|
||||||
|
|
||||||
const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||||
if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` });
|
||||||
|
|
||||||
|
for await (const { role: requestedRoleChange } of roles) {
|
||||||
|
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
|
||||||
|
requestedRoleChange,
|
||||||
|
project.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
|
||||||
|
if (!hasRequiredPrivileges) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
const customInputRoles = roles.filter(
|
const customInputRoles = roles.filter(
|
||||||
@ -223,7 +276,16 @@ export const groupProjectServiceFactory = ({
|
|||||||
$in: { slug: customInputRoles.map(({ role }) => role) }
|
$in: { slug: customInputRoles.map(({ role }) => role) }
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
|
if (customRoles.length !== customInputRoles.length) {
|
||||||
|
const customRoleSlugs = customRoles.map((customRole) => customRole.slug);
|
||||||
|
const missingInputRoles = customInputRoles
|
||||||
|
.filter((inputRole) => !customRoleSlugs.includes(inputRole.role))
|
||||||
|
.map((role) => role.role);
|
||||||
|
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Custom role/s not found: ${missingInputRoles.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
@ -260,24 +322,22 @@ export const groupProjectServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeGroupFromProject = async ({
|
const removeGroupFromProject = async ({
|
||||||
projectSlug,
|
projectId,
|
||||||
groupSlug,
|
groupId,
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TDeleteProjectGroupDTO) => {
|
}: TDeleteProjectGroupDTO) => {
|
||||||
const project = await projectDAL.findOne({
|
const project = await projectDAL.findById(projectId);
|
||||||
slug: projectSlug
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
if (!project) throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` });
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
|
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
|
||||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
if (!group) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` });
|
||||||
|
|
||||||
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||||
if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
|
if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -311,17 +371,17 @@ export const groupProjectServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listGroupsInProject = async ({
|
const listGroupsInProject = async ({
|
||||||
projectSlug,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TListProjectGroupDTO) => {
|
}: TListProjectGroupDTO) => {
|
||||||
const project = await projectDAL.findOne({
|
const project = await projectDAL.findById(projectId);
|
||||||
slug: projectSlug
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
|
if (!project) {
|
||||||
|
throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` });
|
||||||
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -336,10 +396,47 @@ export const groupProjectServiceFactory = ({
|
|||||||
return groupMemberships;
|
return groupMemberships;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGroupInProject = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
groupId,
|
||||||
|
projectId
|
||||||
|
}: TGetGroupInProjectDTO) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
|
const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, {
|
||||||
|
groupId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!groupMembership) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Cannot find group membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupMembership;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addGroupToProject,
|
addGroupToProject,
|
||||||
updateGroupInProject,
|
updateGroupInProject,
|
||||||
removeGroupFromProject,
|
removeGroupFromProject,
|
||||||
listGroupsInProject
|
listGroupsInProject,
|
||||||
|
getGroupInProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,23 @@
|
|||||||
import { TProjectSlugPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
|
||||||
export type TCreateProjectGroupDTO = {
|
export type TCreateProjectGroupDTO = {
|
||||||
groupSlug: string;
|
groupId: string;
|
||||||
|
roles: (
|
||||||
|
| {
|
||||||
role: string;
|
role: string;
|
||||||
} & TProjectSlugPermission;
|
isTemporary?: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
role: string;
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectGroupDTO = {
|
export type TUpdateProjectGroupDTO = {
|
||||||
roles: (
|
roles: (
|
||||||
@ -21,11 +33,13 @@ export type TUpdateProjectGroupDTO = {
|
|||||||
temporaryAccessStartTime: string;
|
temporaryAccessStartTime: string;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
groupSlug: string;
|
groupId: string;
|
||||||
} & TProjectSlugPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteProjectGroupDTO = {
|
export type TDeleteProjectGroupDTO = {
|
||||||
groupSlug: string;
|
groupId: string;
|
||||||
} & TProjectSlugPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TListProjectGroupDTO = TProjectSlugPermission;
|
export type TListProjectGroupDTO = TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetGroupInProjectDTO = TProjectPermission & { groupId: string };
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
infisicalSymmetricDecrypt,
|
infisicalSymmetricDecrypt,
|
||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
@ -68,12 +68,12 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
identityId: identityOidcAuth.identityId
|
identityId: identityOidcAuth.identityId
|
||||||
});
|
});
|
||||||
if (!identityMembershipOrg) {
|
if (!identityMembershipOrg) {
|
||||||
throw new BadRequestError({ message: "Failed to find identity" });
|
throw new NotFoundError({ message: "Failed to find identity in organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||||
if (!orgBot) {
|
if (!orgBot) {
|
||||||
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
throw new NotFoundError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = infisicalSymmetricDecrypt({
|
const key = infisicalSymmetricDecrypt({
|
||||||
@ -106,7 +106,7 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
|
|
||||||
const decodedToken = jwt.decode(oidcJwt, { complete: true });
|
const decodedToken = jwt.decode(oidcJwt, { complete: true });
|
||||||
if (!decodedToken) {
|
if (!decodedToken) {
|
||||||
throw new BadRequestError({
|
throw new UnauthorizedError({
|
||||||
message: "Invalid JWT"
|
message: "Invalid JWT"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -119,13 +119,24 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
const { kid } = decodedToken.header;
|
const { kid } = decodedToken.header;
|
||||||
const oidcSigningKey = await client.getSigningKey(kid);
|
const oidcSigningKey = await client.getSigningKey(kid);
|
||||||
|
|
||||||
const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
|
let tokenData: Record<string, string>;
|
||||||
|
try {
|
||||||
|
tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
|
||||||
issuer: identityOidcAuth.boundIssuer
|
issuer: identityOidcAuth.boundIssuer
|
||||||
}) as Record<string, string>;
|
}) as Record<string, string>;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.JsonWebTokenError) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `Access denied: ${error.message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
if (identityOidcAuth.boundSubject) {
|
if (identityOidcAuth.boundSubject) {
|
||||||
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
|
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
|
||||||
throw new ForbiddenRequestError({
|
throw new UnauthorizedError({
|
||||||
message: "Access denied: OIDC subject not allowed."
|
message: "Access denied: OIDC subject not allowed."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -137,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
.split(", ")
|
.split(", ")
|
||||||
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
|
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenRequestError({
|
throw new UnauthorizedError({
|
||||||
message: "Access denied: OIDC audience not allowed."
|
message: "Access denied: OIDC audience not allowed."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -150,7 +161,7 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
if (
|
if (
|
||||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
|
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenRequestError({
|
throw new UnauthorizedError({
|
||||||
message: "Access denied: OIDC claim not allowed."
|
message: "Access denied: OIDC claim not allowed."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
name: "Admin",
|
name: "Admin",
|
||||||
slug: "admin",
|
slug: "admin",
|
||||||
description: "Complete administration access over the organization",
|
description: "Complete administration access over the organization",
|
||||||
permissions: packRules(orgAdminPermissions.rules),
|
permissions: packRules(orgAdminPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
@ -72,7 +72,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
name: "Member",
|
name: "Member",
|
||||||
slug: "member",
|
slug: "member",
|
||||||
description: "Non-administrative role in an organization",
|
description: "Non-administrative role in an organization",
|
||||||
permissions: packRules(orgMemberPermissions.rules),
|
permissions: packRules(orgMemberPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
@ -84,7 +84,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
name: "No Access",
|
name: "No Access",
|
||||||
slug: "no-access",
|
slug: "no-access",
|
||||||
description: "No access to any resources in the organization",
|
description: "No access to any resources in the organization",
|
||||||
permissions: packRules(orgNoAccessPermissions.rules),
|
permissions: packRules(orgNoAccessPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
@ -151,7 +151,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
name: "Admin",
|
name: "Admin",
|
||||||
slug: "admin",
|
slug: "admin",
|
||||||
description: "Complete administration access over the organization",
|
description: "Complete administration access over the organization",
|
||||||
permissions: packRules(orgAdminPermissions.rules),
|
permissions: packRules(orgAdminPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
@ -161,7 +161,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
name: "Member",
|
name: "Member",
|
||||||
slug: "member",
|
slug: "member",
|
||||||
description: "Non-administrative role in an organization",
|
description: "Non-administrative role in an organization",
|
||||||
permissions: packRules(orgMemberPermissions.rules),
|
permissions: packRules(orgMemberPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
@ -171,7 +171,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
|||||||
name: "No Access",
|
name: "No Access",
|
||||||
slug: "no-access",
|
slug: "no-access",
|
||||||
description: "No access to any resources in the organization",
|
description: "No access to any resources in the organization",
|
||||||
permissions: packRules(orgNoAccessPermissions.rules),
|
permissions: packRules(orgNoAccessPermissions),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
|
@ -458,12 +458,6 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
|
||||||
if (org?.authEnforced) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to invite user due to org-level auth enforced for organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
||||||
if (isEmailInvalid) {
|
if (isEmailInvalid) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@ -472,20 +466,6 @@ export const orgServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
|
||||||
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
|
|
||||||
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
|
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
|
||||||
if (isCustomOrgRole) {
|
if (isCustomOrgRole) {
|
||||||
if (!plan?.rbac)
|
if (!plan?.rbac)
|
||||||
@ -570,7 +550,7 @@ export const orgServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [inviteeMembership] = await orgDAL.findMembership(
|
const [inviteeOrgMembership] = await orgDAL.findMembership(
|
||||||
{
|
{
|
||||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId
|
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId
|
||||||
@ -579,7 +559,27 @@ export const orgServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// if there exist no org membership we set is as given by the request
|
// if there exist no org membership we set is as given by the request
|
||||||
if (!inviteeMembership) {
|
if (!inviteeOrgMembership) {
|
||||||
|
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||||
|
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
|
||||||
|
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org?.authEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to invite user due to org-level auth enforced for organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// as its used by project invite also
|
// as its used by project invite also
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
let roleId;
|
let roleId;
|
||||||
|
@ -26,7 +26,10 @@ export const getBotKeyFnFactory = (
|
|||||||
) => {
|
) => {
|
||||||
const getBotKeyFn = async (projectId: string) => {
|
const getBotKeyFn = async (projectId: string) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." });
|
if (!project)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project not found during bot lookup. Are you sure you are using the correct project ID?"
|
||||||
|
});
|
||||||
|
|
||||||
if (project.version === 3) {
|
if (project.version === 3) {
|
||||||
return { project, shouldUseSecretV2Bridge: true };
|
return { project, shouldUseSecretV2Bridge: true };
|
||||||
|
@ -8,7 +8,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
@ -129,7 +129,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username });
|
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username });
|
||||||
if (!membership) throw new BadRequestError({ message: `Project membership not found for user ${username}` });
|
if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${username}` });
|
||||||
return membership;
|
return membership;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import { TableName, TProjectEnvironments, TSecretFolders, TSecretFoldersUpdate }
|
|||||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { groupBy, removeTrailingSlash } from "@app/lib/fn";
|
import { groupBy, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
export const validateFolderName = (folderName: string) => {
|
export const validateFolderName = (folderName: string) => {
|
||||||
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
||||||
@ -83,7 +85,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
|
|||||||
.from<TSecretFolders & { depth: number; path: string }>("parent");
|
.from<TSecretFolders & { depth: number; path: string }>("parent");
|
||||||
};
|
};
|
||||||
|
|
||||||
const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: string, secretPath: string) => {
|
const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: string[], secretPath: string) => {
|
||||||
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
|
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
|
||||||
const formatedPath = secretPath.at(-1) === "/" && secretPath.length > 1 ? secretPath.slice(0, -1) : secretPath;
|
const formatedPath = secretPath.at(-1) === "/" && secretPath.length > 1 ? secretPath.slice(0, -1) : secretPath;
|
||||||
// next goal to sanitize saw the raw sql query is safe
|
// next goal to sanitize saw the raw sql query is safe
|
||||||
@ -111,7 +113,7 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: stri
|
|||||||
projectId,
|
projectId,
|
||||||
parentId: null
|
parentId: null
|
||||||
})
|
})
|
||||||
.where(`${TableName.Environment}.slug`, environment)
|
.whereIn(`${TableName.Environment}.slug`, environments)
|
||||||
.select(selectAllTableCols(TableName.SecretFolder))
|
.select(selectAllTableCols(TableName.SecretFolder))
|
||||||
.union(
|
.union(
|
||||||
(qb) =>
|
(qb) =>
|
||||||
@ -139,14 +141,14 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: stri
|
|||||||
.from<TSecretFolders & { depth: number; path: string }>("parent")
|
.from<TSecretFolders & { depth: number; path: string }>("parent")
|
||||||
.leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId")
|
.leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId")
|
||||||
.select<
|
.select<
|
||||||
TSecretFolders & {
|
(TSecretFolders & {
|
||||||
depth: number;
|
depth: number;
|
||||||
path: string;
|
path: string;
|
||||||
envId: string;
|
envId: string;
|
||||||
envSlug: string;
|
envSlug: string;
|
||||||
envName: string;
|
envName: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
})[]
|
||||||
>(
|
>(
|
||||||
selectAllTableCols("parent" as TableName.SecretFolder),
|
selectAllTableCols("parent" as TableName.SecretFolder),
|
||||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
@ -214,7 +216,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
const folder = await sqlFindFolderByPathQuery(
|
const folder = await sqlFindFolderByPathQuery(
|
||||||
tx || db.replicaNode(),
|
tx || db.replicaNode(),
|
||||||
projectId,
|
projectId,
|
||||||
environment,
|
[environment],
|
||||||
removeTrailingSlash(path)
|
removeTrailingSlash(path)
|
||||||
)
|
)
|
||||||
.orderBy("depth", "desc")
|
.orderBy("depth", "desc")
|
||||||
@ -230,6 +232,35 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// finds folders by path for multiple envs
|
||||||
|
const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1;
|
||||||
|
|
||||||
|
const folders = await sqlFindFolderByPathQuery(
|
||||||
|
tx || db.replicaNode(),
|
||||||
|
projectId,
|
||||||
|
environments,
|
||||||
|
removeTrailingSlash(path)
|
||||||
|
)
|
||||||
|
.orderBy("depth", "desc")
|
||||||
|
.where("depth", pathDepth);
|
||||||
|
|
||||||
|
const firstFolder = folders[0];
|
||||||
|
|
||||||
|
if (firstFolder && firstFolder.path !== removeTrailingSlash(path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders.map((folder) => {
|
||||||
|
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
|
||||||
|
return { ...el, envId: id, environment: { id, name, slug } };
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find folders by secret path multi env" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// used in folder creation
|
// used in folder creation
|
||||||
// even if its the original given /path1/path2
|
// even if its the original given /path1/path2
|
||||||
// it will stop automatically at /path2
|
// it will stop automatically at /path2
|
||||||
@ -238,7 +269,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
const folder = await sqlFindFolderByPathQuery(
|
const folder = await sqlFindFolderByPathQuery(
|
||||||
tx || db.replicaNode(),
|
tx || db.replicaNode(),
|
||||||
projectId,
|
projectId,
|
||||||
environment,
|
[environment],
|
||||||
removeTrailingSlash(path)
|
removeTrailingSlash(path)
|
||||||
)
|
)
|
||||||
.orderBy("depth", "desc")
|
.orderBy("depth", "desc")
|
||||||
@ -352,14 +383,77 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// find project folders for multiple envs
|
||||||
|
const findByMultiEnv = async (
|
||||||
|
{
|
||||||
|
environmentIds,
|
||||||
|
parentIds,
|
||||||
|
search,
|
||||||
|
limit,
|
||||||
|
offset = 0,
|
||||||
|
orderBy = SecretsOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC
|
||||||
|
}: {
|
||||||
|
environmentIds: string[];
|
||||||
|
parentIds: string[];
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.SecretFolder)
|
||||||
|
.whereIn("parentId", parentIds)
|
||||||
|
.whereIn("envId", environmentIds)
|
||||||
|
.where("isReserved", false)
|
||||||
|
.where((bd) => {
|
||||||
|
if (search) {
|
||||||
|
void bd.whereILike(`${TableName.SecretFolder}.name`, `%${search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretFolder),
|
||||||
|
db.raw(
|
||||||
|
`DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" ${
|
||||||
|
orderDirection ?? OrderByDirection.ASC
|
||||||
|
}) as rank`
|
||||||
|
),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("environment")
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.SecretFolder}.${orderBy}`, orderDirection);
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
const rankOffset = offset + 1; // ranks start from 1
|
||||||
|
return await (tx || db)
|
||||||
|
.with("w", query)
|
||||||
|
.select("*")
|
||||||
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
|
.where("w.rank", ">=", rankOffset)
|
||||||
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await query;
|
||||||
|
|
||||||
|
return folders;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find folders multi env" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretFolderOrm,
|
...secretFolderOrm,
|
||||||
update,
|
update,
|
||||||
findBySecretPath,
|
findBySecretPath,
|
||||||
|
findBySecretPathMultiEnv,
|
||||||
findById,
|
findById,
|
||||||
findByManySecretPath,
|
findByManySecretPath,
|
||||||
findSecretPathByFolderIds,
|
findSecretPathByFolderIds,
|
||||||
findClosestFolder,
|
findClosestFolder,
|
||||||
findByProjectId
|
findByProjectId,
|
||||||
|
findByMultiEnv
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -7,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
@ -26,7 +27,7 @@ type TSecretFolderServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
};
|
};
|
||||||
@ -396,7 +397,12 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
environment,
|
environment,
|
||||||
path: secretPath
|
path: secretPath,
|
||||||
|
search,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
}: TGetFolderDTO) => {
|
}: TGetFolderDTO) => {
|
||||||
// folder list is allowed to be read by anyone
|
// folder list is allowed to be read by anyone
|
||||||
// permission to check does user has access
|
// permission to check does user has access
|
||||||
@ -408,11 +414,92 @@ export const secretFolderServiceFactory = ({
|
|||||||
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!parentFolder) return [];
|
if (!parentFolder) return [];
|
||||||
|
|
||||||
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id, isReserved: false });
|
const folders = await folderDAL.find(
|
||||||
|
{
|
||||||
|
envId: env.id,
|
||||||
|
parentId: parentFolder.id,
|
||||||
|
isReserved: false,
|
||||||
|
$search: search ? { name: `%${search}%` } : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: orderBy ? [[orderBy, orderDirection ?? OrderByDirection.ASC]] : undefined,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return folders;
|
||||||
|
};
|
||||||
|
|
||||||
|
// get folders for multiple envs
|
||||||
|
const getFoldersMultiEnv = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environments,
|
||||||
|
path: secretPath,
|
||||||
|
...params
|
||||||
|
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
|
||||||
|
// folder list is allowed to be read by anyone
|
||||||
|
// permission to check does user has access
|
||||||
|
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||||
|
|
||||||
|
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
|
||||||
|
|
||||||
|
if (!envs.length)
|
||||||
|
throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" });
|
||||||
|
|
||||||
|
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
||||||
|
if (!parentFolders.length) return [];
|
||||||
|
|
||||||
|
const folders = await folderDAL.findByMultiEnv({
|
||||||
|
environmentIds: envs.map((env) => env.id),
|
||||||
|
parentIds: parentFolders.map((folder) => folder.id),
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
|
||||||
return folders;
|
return folders;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// get the unique count of folders within a project path
|
||||||
|
const getProjectFolderCount = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environments,
|
||||||
|
path: secretPath,
|
||||||
|
search
|
||||||
|
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
|
||||||
|
// folder list is allowed to be read by anyone
|
||||||
|
// permission to check does user has access
|
||||||
|
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||||
|
|
||||||
|
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
|
||||||
|
|
||||||
|
if (!envs.length)
|
||||||
|
throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" });
|
||||||
|
|
||||||
|
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
||||||
|
if (!parentFolders.length) return 0;
|
||||||
|
|
||||||
|
const folders = await folderDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
envId: envs.map((env) => env.id),
|
||||||
|
parentId: parentFolders.map((folder) => folder.id)
|
||||||
|
},
|
||||||
|
isReserved: false,
|
||||||
|
$search: search ? { name: `%${search}%` } : undefined
|
||||||
|
},
|
||||||
|
{ countDistinct: "name" }
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number(folders[0]?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => {
|
const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => {
|
||||||
const folder = await folderDAL.findById(id);
|
const folder = await folderDAL.findById(id);
|
||||||
if (!folder) throw new NotFoundError({ message: "folder not found" });
|
if (!folder) throw new NotFoundError({ message: "folder not found" });
|
||||||
@ -429,6 +516,8 @@ export const secretFolderServiceFactory = ({
|
|||||||
updateManyFolders,
|
updateManyFolders,
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
getFolders,
|
getFolders,
|
||||||
getFolderById
|
getFolderById,
|
||||||
|
getProjectFolderCount,
|
||||||
|
getFoldersMultiEnv
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
export enum ReservedFolders {
|
export enum ReservedFolders {
|
||||||
SecretReplication = "__reserve_replication_"
|
SecretReplication = "__reserve_replication_"
|
||||||
@ -36,6 +37,11 @@ export type TDeleteFolderDTO = {
|
|||||||
export type TGetFolderDTO = {
|
export type TGetFolderDTO = {
|
||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
search?: string;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetFolderByIdDTO = {
|
export type TGetFolderByIdDTO = {
|
||||||
|
@ -49,10 +49,30 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const find = async (filter: Partial<TSecretImports & { projectId: string }>, tx?: Knex) => {
|
const find = async (
|
||||||
|
{
|
||||||
|
search,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
...filter
|
||||||
|
}: Partial<
|
||||||
|
TSecretImports & {
|
||||||
|
projectId: string;
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
const query = (tx || db.replicaNode())(TableName.SecretImport)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
|
.where((bd) => {
|
||||||
|
if (search) {
|
||||||
|
void bd.whereILike("importPath", `%${search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
|
||||||
.select(
|
.select(
|
||||||
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
|
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
|
||||||
@ -61,6 +81,13 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("id").withSchema(TableName.Environment).as("envId")
|
db.ref("id").withSchema(TableName.Environment).as("envId")
|
||||||
)
|
)
|
||||||
.orderBy("position", "asc");
|
.orderBy("position", "asc");
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
void query.limit(limit).offset(offset ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
|
||||||
return docs.map(({ envId, slug, name, ...el }) => ({
|
return docs.map(({ envId, slug, name, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
importEnv: { id: envId, slug, name }
|
importEnv: { id: envId, slug, name }
|
||||||
@ -70,6 +97,28 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectImportCount = async (
|
||||||
|
{ search, ...filter }: Partial<TSecretImports & { projectId: string; search?: string }>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||||
|
.where(filter)
|
||||||
|
.where("isReplication", false)
|
||||||
|
.where((bd) => {
|
||||||
|
if (search) {
|
||||||
|
void bd.whereILike("importPath", `%${search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
return Number(docs[0]?.count ?? 0);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "get secret imports count" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findByFolderIds = async (folderIds: string[], tx?: Knex) => {
|
const findByFolderIds = async (folderIds: string[], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||||
@ -97,6 +146,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
find,
|
find,
|
||||||
findByFolderIds,
|
findByFolderIds,
|
||||||
findLastImportPosition,
|
findLastImportPosition,
|
||||||
updateAllPosition
|
updateAllPosition,
|
||||||
|
getProjectImportCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -220,7 +220,7 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
|
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
|
||||||
|
|
||||||
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
|
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
|
||||||
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0];
|
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0];
|
||||||
const folderDeeperImportSecrets =
|
const folderDeeperImportSecrets =
|
||||||
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
|
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
|
||||||
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
|
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
|
||||||
|
@ -7,7 +7,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
|
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
@ -394,6 +394,36 @@ export const secretImportServiceFactory = ({
|
|||||||
return { message: "replication started" };
|
return { message: "replication started" };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectImportCount = async ({
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
search
|
||||||
|
}: TGetSecretImportsDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
|
if (!folder) throw new NotFoundError({ message: "Folder not found", name: "Get imports" });
|
||||||
|
|
||||||
|
const count = await secretImportDAL.getProjectImportCount({ folderId: folder.id, search });
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
const getImports = async ({
|
const getImports = async ({
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
@ -401,7 +431,10 @@ export const secretImportServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
search,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
}: TGetSecretImportsDTO) => {
|
}: TGetSecretImportsDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -418,7 +451,7 @@ export const secretImportServiceFactory = ({
|
|||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" });
|
||||||
|
|
||||||
const secImports = await secretImportDAL.find({ folderId: folder.id });
|
const secImports = await secretImportDAL.find({ folderId: folder.id, search, limit, offset });
|
||||||
return secImports;
|
return secImports;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -512,7 +545,11 @@ export const secretImportServiceFactory = ({
|
|||||||
return importedSecrets;
|
return importedSecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
|
|
||||||
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||||
return importedSecrets.map((el) => ({
|
return importedSecrets.map((el) => ({
|
||||||
@ -531,6 +568,7 @@ export const secretImportServiceFactory = ({
|
|||||||
getSecretsFromImports,
|
getSecretsFromImports,
|
||||||
getRawSecretsFromImports,
|
getRawSecretsFromImports,
|
||||||
resyncSecretImportReplication,
|
resyncSecretImportReplication,
|
||||||
|
getProjectImportCount,
|
||||||
fnSecretsFromImports
|
fnSecretsFromImports
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -32,6 +32,9 @@ export type TDeleteSecretImportDTO = {
|
|||||||
export type TGetSecretImportsDTO = {
|
export type TGetSecretImportsDTO = {
|
||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetSecretsFromImportDTO = {
|
export type TGetSecretsFromImportDTO = {
|
||||||
|
@ -5,6 +5,8 @@ import { TDbClient } from "@app/db";
|
|||||||
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
|
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
|
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
|
||||||
|
|
||||||
@ -181,7 +183,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
|
// get unique secret count by folder IDs
|
||||||
|
const countByFolderIds = async (
|
||||||
|
folderIds: string[],
|
||||||
|
userId?: string,
|
||||||
|
tx?: Knex,
|
||||||
|
filters?: {
|
||||||
|
search?: string;
|
||||||
|
tagSlugs?: string[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||||
if (userId && !uuidValidate(userId)) {
|
if (userId && !uuidValidate(userId)) {
|
||||||
@ -189,8 +200,70 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
userId = undefined;
|
userId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
|
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||||
.whereIn("folderId", folderIds)
|
.whereIn("folderId", folderIds)
|
||||||
|
.where((bd) => {
|
||||||
|
if (filters?.search) {
|
||||||
|
void bd.whereILike("key", `%${filters?.search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where((bd) => {
|
||||||
|
void bd.whereNull("userId").orWhere({ userId: userId || null });
|
||||||
|
})
|
||||||
|
.countDistinct("key");
|
||||||
|
|
||||||
|
// only need to join tags if filtering by tag slugs
|
||||||
|
const slugs = filters?.tagSlugs?.filter(Boolean);
|
||||||
|
if (slugs && slugs.length > 0) {
|
||||||
|
void query
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretV2JnTag,
|
||||||
|
`${TableName.SecretV2}.id`,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretTag,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
|
`${TableName.SecretTag}.id`
|
||||||
|
)
|
||||||
|
.whereIn("slug", slugs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const secrets = await query;
|
||||||
|
|
||||||
|
return Number(secrets[0]?.count ?? 0);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "get folder secret count" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByFolderIds = async (
|
||||||
|
folderIds: string[],
|
||||||
|
userId?: string,
|
||||||
|
tx?: Knex,
|
||||||
|
filters?: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
tagSlugs?: string[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||||
|
if (userId && !uuidValidate(userId)) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
userId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||||
|
.whereIn("folderId", folderIds)
|
||||||
|
.where((bd) => {
|
||||||
|
if (filters?.search) {
|
||||||
|
void bd.whereILike("key", `%${filters?.search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
void bd.whereNull("userId").orWhere({ userId: userId || null });
|
void bd.whereNull("userId").orWhere({ userId: userId || null });
|
||||||
})
|
})
|
||||||
@ -204,11 +277,37 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
`${TableName.SecretTag}.id`
|
`${TableName.SecretTag}.id`
|
||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretV2))
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretV2),
|
||||||
|
db.raw(`DENSE_RANK() OVER (ORDER BY "key" ${filters?.orderDirection ?? OrderByDirection.ASC}) as rank`)
|
||||||
|
)
|
||||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||||
.orderBy("id", "asc");
|
.where((bd) => {
|
||||||
|
const slugs = filters?.tagSlugs?.filter(Boolean);
|
||||||
|
if (slugs && slugs.length > 0) {
|
||||||
|
void bd.whereIn("slug", slugs);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.orderBy(
|
||||||
|
filters?.orderBy === SecretsOrderBy.Name ? "key" : "id",
|
||||||
|
filters?.orderDirection ?? OrderByDirection.ASC
|
||||||
|
);
|
||||||
|
|
||||||
|
let secs: Awaited<typeof query>;
|
||||||
|
|
||||||
|
if (filters?.limit) {
|
||||||
|
const rankOffset = (filters?.offset ?? 0) + 1; // ranks start at 1
|
||||||
|
secs = await (tx || db)
|
||||||
|
.with("w", query)
|
||||||
|
.select("*")
|
||||||
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
|
.where("w.rank", ">=", rankOffset)
|
||||||
|
.andWhere("w.rank", "<", rankOffset + filters.limit);
|
||||||
|
} else {
|
||||||
|
secs = await query;
|
||||||
|
}
|
||||||
|
|
||||||
const data = sqlNestRelationships({
|
const data = sqlNestRelationships({
|
||||||
data: secs,
|
data: secs,
|
||||||
@ -384,6 +483,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
findBySecretKeys,
|
findBySecretKeys,
|
||||||
upsertSecretReferences,
|
upsertSecretReferences,
|
||||||
findReferencedSecretReferences,
|
findReferencedSecretReferences,
|
||||||
findAllProjectSecretValues
|
findAllProjectSecretValues,
|
||||||
|
countByFolderIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
||||||
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
@ -375,6 +376,7 @@ type TInterpolateSecretArg = {
|
|||||||
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
|
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
|
canExpandValue: (environment: string, secretPath: string) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_SECRET_REFERENCE_DEPTH = 10;
|
const MAX_SECRET_REFERENCE_DEPTH = 10;
|
||||||
@ -382,7 +384,8 @@ export const expandSecretReferencesFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
decryptSecretValue: decryptSecret,
|
decryptSecretValue: decryptSecret,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
folderDAL
|
folderDAL,
|
||||||
|
canExpandValue
|
||||||
}: TInterpolateSecretArg) => {
|
}: TInterpolateSecretArg) => {
|
||||||
const secretCache: Record<string, Record<string, string>> = {};
|
const secretCache: Record<string, Record<string, string>> = {};
|
||||||
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
||||||
@ -432,6 +435,11 @@ export const expandSecretReferencesFactory = ({
|
|||||||
if (entities.length === 1) {
|
if (entities.length === 1) {
|
||||||
const [secretKey] = entities;
|
const [secretKey] = entities;
|
||||||
|
|
||||||
|
if (!canExpandValue(environment, secretPath))
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.`
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-continue,no-await-in-loop
|
// eslint-disable-next-line no-continue,no-await-in-loop
|
||||||
const referedValue = await fetchSecret(environment, secretPath, secretKey);
|
const referedValue = await fetchSecret(environment, secretPath, secretKey);
|
||||||
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
||||||
@ -452,6 +460,11 @@ export const expandSecretReferencesFactory = ({
|
|||||||
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
|
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
|
||||||
const secretReferenceKey = entities[entities.length - 1];
|
const secretReferenceKey = entities[entities.length - 1];
|
||||||
|
|
||||||
|
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath))
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.`
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
|
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
|
||||||
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
||||||
|
@ -59,7 +59,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
folderDAL: Pick<
|
folderDAL: Pick<
|
||||||
TSecretFolderDALFactory,
|
TSecretFolderDALFactory,
|
||||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
|
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
|
||||||
>;
|
>;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
||||||
@ -152,6 +152,15 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
references.forEach((referredSecret) => {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: referredSecret.environment,
|
||||||
|
secretPath: referredSecret.secretPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const secret = await secretDAL.transaction((tx) =>
|
const secret = await secretDAL.transaction((tx) =>
|
||||||
fnSecretBulkInsert({
|
fnSecretBulkInsert({
|
||||||
@ -292,6 +301,17 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
references: getAllNestedSecretReferences(secretValue)
|
references: getAllNestedSecretReferences(secretValue)
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
if (encryptedValue.references) {
|
||||||
|
encryptedValue.references.forEach((referredSecret) => {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: referredSecret.environment,
|
||||||
|
secretPath: referredSecret.secretPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
||||||
fnSecretBulkUpdate({
|
fnSecretBulkUpdate({
|
||||||
@ -431,6 +451,165 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// get unique secrets count for multiple envs
|
||||||
|
const getSecretsCountMultiEnv = async ({
|
||||||
|
actorId,
|
||||||
|
path,
|
||||||
|
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environments,
|
||||||
|
...params
|
||||||
|
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
||||||
|
environments: string[];
|
||||||
|
}) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify user has access to all environments
|
||||||
|
environments.forEach((environment) =>
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
||||||
|
if (!folders.length) return 0;
|
||||||
|
|
||||||
|
const count = await secretDAL.countByFolderIds(
|
||||||
|
folders.map((folder) => folder.id),
|
||||||
|
actorId,
|
||||||
|
undefined,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
// get secret count for individual env
|
||||||
|
const getSecretsCount = async ({
|
||||||
|
actorId,
|
||||||
|
path,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
...params
|
||||||
|
}: Pick<
|
||||||
|
TGetSecretsDTO,
|
||||||
|
| "actorId"
|
||||||
|
| "actor"
|
||||||
|
| "path"
|
||||||
|
| "projectId"
|
||||||
|
| "actorOrgId"
|
||||||
|
| "actorAuthMethod"
|
||||||
|
| "tagSlugs"
|
||||||
|
| "environment"
|
||||||
|
| "search"
|
||||||
|
>) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) return 0;
|
||||||
|
|
||||||
|
const count = await secretDAL.countByFolderIds([folder.id], actorId, undefined, params);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
// get secrets for multiple envs
|
||||||
|
const getSecretsMultiEnv = async ({
|
||||||
|
actorId,
|
||||||
|
path,
|
||||||
|
environments,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
...params
|
||||||
|
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
||||||
|
environments: string[];
|
||||||
|
}) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
let paths: { folderId: string; path: string; environment: string }[] = [];
|
||||||
|
|
||||||
|
// verify user has access to all environments
|
||||||
|
environments.forEach((environment) =>
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
||||||
|
|
||||||
|
if (!folders.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug }));
|
||||||
|
|
||||||
|
const groupedPaths = groupBy(paths, (p) => p.folderId);
|
||||||
|
|
||||||
|
const secrets = await secretDAL.findByFolderIds(
|
||||||
|
paths.map((p) => p.folderId),
|
||||||
|
actorId,
|
||||||
|
undefined,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecrets = secrets.map((secret) =>
|
||||||
|
reshapeBridgeSecret(
|
||||||
|
projectId,
|
||||||
|
groupedPaths[secret.folderId][0].environment,
|
||||||
|
groupedPaths[secret.folderId][0].path,
|
||||||
|
{
|
||||||
|
...secret,
|
||||||
|
value: secret.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||||
|
: "",
|
||||||
|
comment: secret.encryptedComment
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return decryptedSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
const getSecrets = async ({
|
const getSecrets = async ({
|
||||||
actorId,
|
actorId,
|
||||||
path,
|
path,
|
||||||
@ -441,8 +620,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
includeImports,
|
includeImports,
|
||||||
recursive,
|
recursive,
|
||||||
tagSlugs = [],
|
expandSecretReferences: shouldExpandSecretReferences,
|
||||||
expandSecretReferences: shouldExpandSecretReferences
|
...params
|
||||||
}: TGetSecretsDTO) => {
|
}: TGetSecretsDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -490,7 +669,9 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
|
|
||||||
const secrets = await secretDAL.findByFolderIds(
|
const secrets = await secretDAL.findByFolderIds(
|
||||||
paths.map((p) => p.folderId),
|
paths.map((p) => p.folderId),
|
||||||
actorId
|
actorId,
|
||||||
|
undefined,
|
||||||
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
@ -509,18 +690,21 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
: ""
|
: ""
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const filteredSecrets = tagSlugs.length
|
|
||||||
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
|
||||||
: decryptedSecrets;
|
|
||||||
const expandSecretReferences = expandSecretReferencesFactory({
|
const expandSecretReferences = expandSecretReferencesFactory({
|
||||||
projectId,
|
projectId,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
||||||
|
canExpandValue: (expandEnvironment, expandSecretPath) =>
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath })
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldExpandSecretReferences) {
|
if (shouldExpandSecretReferences) {
|
||||||
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
|
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
Object.keys(secretsGroupByPath).map((groupedPath) =>
|
Object.keys(secretsGroupByPath).map((groupedPath) =>
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
@ -541,7 +725,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
|
|
||||||
if (!includeImports) {
|
if (!includeImports) {
|
||||||
return {
|
return {
|
||||||
secrets: filteredSecrets
|
secrets: decryptedSecrets
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -569,7 +753,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secrets: filteredSecrets,
|
secrets: decryptedSecrets,
|
||||||
imports: importedSecrets
|
imports: importedSecrets
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -640,7 +824,12 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
||||||
|
canExpandValue: (expandEnvironment, expandSecretPath) =>
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath })
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
// now if secret is not found
|
// now if secret is not found
|
||||||
@ -757,7 +946,19 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
|
|
||||||
const newSecrets = await secretDAL.transaction(async (tx) =>
|
const newSecrets = await secretDAL.transaction(async (tx) =>
|
||||||
fnSecretBulkInsert({
|
fnSecretBulkInsert({
|
||||||
inputSecrets: inputSecrets.map((el) => ({
|
inputSecrets: inputSecrets.map((el) => {
|
||||||
|
const references = getAllNestedSecretReferences(el.secretValue);
|
||||||
|
references.forEach((referredSecret) => {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: referredSecret.environment,
|
||||||
|
secretPath: referredSecret.secretPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
encryptedComment: setKnexStringValue(
|
encryptedComment: setKnexStringValue(
|
||||||
el.secretComment,
|
el.secretComment,
|
||||||
@ -769,9 +970,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
key: el.secretKey,
|
key: el.secretKey,
|
||||||
tagIds: el.tagIds,
|
tagIds: el.tagIds,
|
||||||
references: getAllNestedSecretReferences(el.secretValue),
|
references,
|
||||||
type: SecretType.Shared
|
type: SecretType.Shared
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
folderId,
|
folderId,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
@ -878,6 +1080,19 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
references: getAllNestedSecretReferences(el.secretValue)
|
references: getAllNestedSecretReferences(el.secretValue)
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
if (encryptedValue.references) {
|
||||||
|
encryptedValue.references.forEach((referredSecret) => {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: referredSecret.environment,
|
||||||
|
secretPath: referredSecret.secretPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filter: { id: originalSecret.id, type: SecretType.Shared },
|
filter: { id: originalSecret.id, type: SecretType.Shared },
|
||||||
data: {
|
data: {
|
||||||
@ -1416,6 +1631,9 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
getSecrets,
|
getSecrets,
|
||||||
getSecretVersions,
|
getSecretVersions,
|
||||||
backfillSecretReferences,
|
backfillSecretReferences,
|
||||||
moveSecrets
|
moveSecrets,
|
||||||
|
getSecretsCount,
|
||||||
|
getSecretsCountMultiEnv,
|
||||||
|
getSecretsMultiEnv
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
|
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
|
||||||
@ -21,6 +22,11 @@ export type TGetSecretsDTO = {
|
|||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretDTO = {
|
export type TGetASecretDTO = {
|
||||||
|
@ -832,7 +832,11 @@ export const createManySecretsRawFnFactory = ({
|
|||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
const inputSecrets = secrets.map((secret) => {
|
const inputSecrets = secrets.map((secret) => {
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
|
||||||
@ -993,7 +997,11 @@ export const updateManySecretsRawFnFactory = ({
|
|||||||
return updatedSecrets;
|
return updatedSecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||||
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||||
|
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas";
|
import {
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectUpgradeStatus,
|
||||||
|
ProjectVersion,
|
||||||
|
TSecretSnapshotSecretsV2,
|
||||||
|
TSecretVersionsV2
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
@ -50,7 +56,9 @@ import { TSecretDALFactory } from "./secret-dal";
|
|||||||
import { interpolateSecrets } from "./secret-fns";
|
import { interpolateSecrets } from "./secret-fns";
|
||||||
import {
|
import {
|
||||||
TCreateSecretReminderDTO,
|
TCreateSecretReminderDTO,
|
||||||
|
TFailedIntegrationSyncEmailsPayload,
|
||||||
THandleReminderDTO,
|
THandleReminderDTO,
|
||||||
|
TIntegrationSyncPayload,
|
||||||
TRemoveSecretReminderDTO,
|
TRemoveSecretReminderDTO,
|
||||||
TSyncSecretsDTO
|
TSyncSecretsDTO
|
||||||
} from "./secret-types";
|
} from "./secret-types";
|
||||||
@ -282,7 +290,9 @@ export const secretQueueFactory = ({
|
|||||||
decryptSecretValue: dto.decryptor,
|
decryptSecretValue: dto.decryptor,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
projectId: dto.projectId
|
projectId: dto.projectId,
|
||||||
|
// on integration expand all secrets
|
||||||
|
canExpandValue: () => true
|
||||||
});
|
});
|
||||||
// process secrets in current folder
|
// process secrets in current folder
|
||||||
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
|
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
|
||||||
@ -509,6 +519,19 @@ export const secretQueueFactory = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.isSmtpConfigured) return;
|
||||||
|
|
||||||
|
await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, {
|
||||||
|
jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`,
|
||||||
|
delay: 1_000 * 60, // 1 minute
|
||||||
|
|
||||||
|
removeOnFail: true,
|
||||||
|
removeOnComplete: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
queueService.start(QueueName.SecretSync, async (job) => {
|
queueService.start(QueueName.SecretSync, async (job) => {
|
||||||
const {
|
const {
|
||||||
_deDupeQueue: deDupeQueue,
|
_deDupeQueue: deDupeQueue,
|
||||||
@ -554,7 +577,46 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.IntegrationSync, async (job) => {
|
queueService.start(QueueName.IntegrationSync, async (job) => {
|
||||||
const { environment, actorId, isManual, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data;
|
if (job.name === QueueJobs.SendFailedIntegrationSyncEmails) {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const jobPayload = job.data as TFailedIntegrationSyncEmailsPayload;
|
||||||
|
|
||||||
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(jobPayload.projectId);
|
||||||
|
const project = await projectDAL.findById(jobPayload.projectId);
|
||||||
|
|
||||||
|
// Only send emails to admins, and if its a manual trigger, only send it to the person who triggered it (if actor is admin as well)
|
||||||
|
const filteredProjectMembers = projectMembers
|
||||||
|
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||||
|
.filter((member) =>
|
||||||
|
jobPayload.manuallyTriggeredByUserId ? member.userId === jobPayload.manuallyTriggeredByUserId : true
|
||||||
|
);
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
recipients: filteredProjectMembers.map((member) => member.user.email!),
|
||||||
|
template: SmtpTemplates.IntegrationSyncFailed,
|
||||||
|
subjectLine: `Integration Sync Failed`,
|
||||||
|
substitutions: {
|
||||||
|
syncMessage: jobPayload.count === 1 ? jobPayload.syncMessage : undefined, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array.
|
||||||
|
secretPath: jobPayload.secretPath,
|
||||||
|
environment: jobPayload.environmentName,
|
||||||
|
count: jobPayload.count,
|
||||||
|
projectName: project.name,
|
||||||
|
integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.name === QueueJobs.IntegrationSync) {
|
||||||
|
const {
|
||||||
|
environment,
|
||||||
|
actorId,
|
||||||
|
isManual,
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
depth = 1,
|
||||||
|
deDupeQueue = {}
|
||||||
|
} = job.data as TIntegrationSyncPayload;
|
||||||
if (depth > MAX_SYNC_SECRET_DEPTH) return;
|
if (depth > MAX_SYNC_SECRET_DEPTH) return;
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
@ -577,7 +639,7 @@ export const secretQueueFactory = ({
|
|||||||
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
||||||
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
|
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
|
||||||
logger.info(
|
logger.info(
|
||||||
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
imports
|
imports
|
||||||
@ -629,7 +691,7 @@ export const secretQueueFactory = ({
|
|||||||
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
|
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
|
||||||
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
|
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
|
||||||
logger.info(
|
logger.info(
|
||||||
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
referencedFolderIds
|
referencedFolderIds
|
||||||
@ -663,9 +725,11 @@ export const secretQueueFactory = ({
|
|||||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = [];
|
||||||
|
|
||||||
if (!integrations.length) return;
|
if (!integrations.length) return;
|
||||||
logger.info(
|
logger.info(
|
||||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
const lock = await keyStore.acquireLock(
|
const lock = await keyStore.acquireLock(
|
||||||
@ -688,7 +752,7 @@ export const secretQueueFactory = ({
|
|||||||
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
|
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
|
||||||
if (isStaleSyncIntegration) {
|
if (isStaleSyncIntegration) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -833,10 +897,18 @@ export const secretQueueFactory = ({
|
|||||||
syncMessage: response?.syncMessage ?? "",
|
syncMessage: response?.syncMessage ?? "",
|
||||||
isSynced: response?.isSynced ?? true
|
isSynced: response?.isSynced ?? true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check.
|
||||||
|
if (response?.isSynced === false) {
|
||||||
|
integrationsFailedToSync.push({
|
||||||
|
integrationId: integration.id,
|
||||||
|
syncMessage: response.syncMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
err,
|
err,
|
||||||
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
|
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
@ -863,10 +935,29 @@ export const secretQueueFactory = ({
|
|||||||
syncMessage: message,
|
syncMessage: message,
|
||||||
isSynced: false
|
isSynced: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
integrationsFailedToSync.push({
|
||||||
|
integrationId: integration.id,
|
||||||
|
syncMessage: message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await lock.release();
|
await lock.release();
|
||||||
|
if (integrationsFailedToSync.length) {
|
||||||
|
await sendFailedIntegrationSyncEmails({
|
||||||
|
count: integrationsFailedToSync.length,
|
||||||
|
environmentName: folder.environment.name,
|
||||||
|
environmentSlug: environment,
|
||||||
|
...(isManual &&
|
||||||
|
actorId && {
|
||||||
|
manuallyTriggeredByUserId: actorId
|
||||||
|
}),
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
syncMessage: integrationsFailedToSync[0].syncMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await keyStore.setItemWithExpiry(
|
await keyStore.setItemWithExpiry(
|
||||||
@ -875,6 +966,7 @@ export const secretQueueFactory = ({
|
|||||||
lockAcquiredTime.toISOString()
|
lockAcquiredTime.toISOString()
|
||||||
);
|
);
|
||||||
logger.info("Secret integration sync ended: %s", job.id);
|
logger.info("Secret integration sync ended: %s", job.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
||||||
|
@ -954,6 +954,120 @@ export const secretServiceFactory = ({
|
|||||||
return secretsDeleted;
|
return secretsDeleted;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSecretsCount = async ({
|
||||||
|
projectId,
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environment,
|
||||||
|
tagSlugs = [],
|
||||||
|
...v2Params
|
||||||
|
}: Pick<
|
||||||
|
TGetSecretsRawDTO,
|
||||||
|
| "projectId"
|
||||||
|
| "path"
|
||||||
|
| "actor"
|
||||||
|
| "actorId"
|
||||||
|
| "actorOrgId"
|
||||||
|
| "actorAuthMethod"
|
||||||
|
| "environment"
|
||||||
|
| "tagSlugs"
|
||||||
|
| "search"
|
||||||
|
>) => {
|
||||||
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
|
if (!shouldUseSecretV2Bridge)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project version does not support pagination",
|
||||||
|
name: "pagination_not_supported"
|
||||||
|
});
|
||||||
|
|
||||||
|
const count = await secretV2BridgeService.getSecretsCount({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
environment,
|
||||||
|
path,
|
||||||
|
actorAuthMethod,
|
||||||
|
tagSlugs,
|
||||||
|
...v2Params
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsCountMultiEnv = async ({
|
||||||
|
projectId,
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environments,
|
||||||
|
...v2Params
|
||||||
|
}: Pick<
|
||||||
|
TGetSecretsRawDTO,
|
||||||
|
"projectId" | "path" | "actor" | "actorId" | "actorOrgId" | "actorAuthMethod" | "search"
|
||||||
|
> & { environments: string[] }) => {
|
||||||
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
|
if (!shouldUseSecretV2Bridge)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project version does not support pagination",
|
||||||
|
name: "pagination_not_supported"
|
||||||
|
});
|
||||||
|
|
||||||
|
const count = await secretV2BridgeService.getSecretsCountMultiEnv({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
environments,
|
||||||
|
path,
|
||||||
|
actorAuthMethod,
|
||||||
|
...v2Params
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsRawMultiEnv = async ({
|
||||||
|
projectId,
|
||||||
|
path,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environments,
|
||||||
|
...params
|
||||||
|
}: Omit<TGetSecretsRawDTO, "environment" | "includeImports" | "expandSecretReferences" | "recursive" | "tagSlugs"> & {
|
||||||
|
environments: string[];
|
||||||
|
}) => {
|
||||||
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
|
if (!shouldUseSecretV2Bridge)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project version does not support pagination",
|
||||||
|
name: "pagination_not_supported"
|
||||||
|
});
|
||||||
|
|
||||||
|
const secrets = await secretV2BridgeService.getSecretsMultiEnv({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
environments,
|
||||||
|
path,
|
||||||
|
actorAuthMethod,
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
const getSecretsRaw = async ({
|
const getSecretsRaw = async ({
|
||||||
projectId,
|
projectId,
|
||||||
path,
|
path,
|
||||||
@ -965,7 +1079,8 @@ export const secretServiceFactory = ({
|
|||||||
includeImports,
|
includeImports,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
recursive,
|
recursive,
|
||||||
tagSlugs = []
|
tagSlugs = [],
|
||||||
|
...paramsV2
|
||||||
}: TGetSecretsRawDTO) => {
|
}: TGetSecretsRawDTO) => {
|
||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
@ -980,12 +1095,17 @@ export const secretServiceFactory = ({
|
|||||||
recursive,
|
recursive,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
includeImports,
|
includeImports,
|
||||||
tagSlugs
|
tagSlugs,
|
||||||
|
...paramsV2
|
||||||
});
|
});
|
||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
|
|
||||||
const { secrets, imports } = await getSecrets({
|
const { secrets, imports } = await getSecrets({
|
||||||
actorId,
|
actorId,
|
||||||
@ -1146,7 +1266,10 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
||||||
|
|
||||||
if (expandSecretReferences) {
|
if (expandSecretReferences) {
|
||||||
@ -1238,7 +1361,11 @@ export const secretServiceFactory = ({
|
|||||||
return { secret, type: SecretProtectionType.Direct as const };
|
return { secret, type: SecretProtectionType.Direct as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
||||||
@ -1376,7 +1503,11 @@ export const secretServiceFactory = ({
|
|||||||
return { type: SecretProtectionType.Direct as const, secret };
|
return { type: SecretProtectionType.Direct as const, secret };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
|
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
||||||
@ -1498,7 +1629,11 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
return { type: SecretProtectionType.Direct as const, secret };
|
return { type: SecretProtectionType.Direct as const, secret };
|
||||||
}
|
}
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
if (policy) {
|
if (policy) {
|
||||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||||
policy,
|
policy,
|
||||||
@ -1598,7 +1733,11 @@ export const secretServiceFactory = ({
|
|||||||
return { secrets, type: SecretProtectionType.Direct as const };
|
return { secrets, type: SecretProtectionType.Direct as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
const sanitizedSecrets = inputSecrets.map(
|
const sanitizedSecrets = inputSecrets.map(
|
||||||
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
|
||||||
@ -1720,7 +1859,11 @@ export const secretServiceFactory = ({
|
|||||||
return { type: SecretProtectionType.Direct as const, secrets };
|
return { type: SecretProtectionType.Direct as const, secrets };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
const sanitizedSecrets = inputSecrets.map(
|
const sanitizedSecrets = inputSecrets.map(
|
||||||
({
|
({
|
||||||
secretComment,
|
secretComment,
|
||||||
@ -1848,7 +1991,11 @@ export const secretServiceFactory = ({
|
|||||||
return { type: SecretProtectionType.Direct as const, secrets };
|
return { type: SecretProtectionType.Direct as const, secrets };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
|
|
||||||
if (policy) {
|
if (policy) {
|
||||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||||
@ -2182,7 +2329,10 @@ export const secretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
|
|
||||||
await secretDAL.transaction(async (tx) => {
|
await secretDAL.transaction(async (tx) => {
|
||||||
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
|
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
|
||||||
@ -2265,7 +2415,10 @@ export const secretServiceFactory = ({
|
|||||||
|
|
||||||
const { botKey } = await projectBotService.getBotKey(project.id);
|
const { botKey } = await projectBotService.getBotKey(project.id);
|
||||||
if (!botKey) {
|
if (!botKey) {
|
||||||
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
throw new BadRequestError({
|
||||||
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
|
name: "bot_not_found_error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
|
const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
|
||||||
@ -2656,6 +2809,9 @@ export const secretServiceFactory = ({
|
|||||||
getSecretVersions,
|
getSecretVersions,
|
||||||
backfillSecretReferences,
|
backfillSecretReferences,
|
||||||
moveSecrets,
|
moveSecrets,
|
||||||
startSecretV2Migration
|
startSecretV2Migration,
|
||||||
|
getSecretsCount,
|
||||||
|
getSecretsCountMultiEnv,
|
||||||
|
getSecretsRawMultiEnv
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
|
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
@ -21,6 +22,29 @@ type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secret
|
|||||||
|
|
||||||
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id">;
|
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id">;
|
||||||
|
|
||||||
|
export const FailedIntegrationSyncEmailsPayloadSchema = z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
secretPath: z.string(),
|
||||||
|
environmentName: z.string(),
|
||||||
|
environmentSlug: z.string(),
|
||||||
|
|
||||||
|
count: z.number(),
|
||||||
|
syncMessage: z.string().optional(),
|
||||||
|
manuallyTriggeredByUserId: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFailedIntegrationSyncEmailsPayload = z.infer<typeof FailedIntegrationSyncEmailsPayloadSchema>;
|
||||||
|
|
||||||
|
export type TIntegrationSyncPayload = {
|
||||||
|
isManual?: boolean;
|
||||||
|
actorId?: string;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
depth?: number;
|
||||||
|
deDupeQueue?: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateSecretDTO = {
|
export type TCreateSecretDTO = {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
path: string;
|
path: string;
|
||||||
@ -81,6 +105,8 @@ export type TGetSecretsDTO = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretDTO = {
|
export type TGetASecretDTO = {
|
||||||
@ -143,6 +169,10 @@ export type TDeleteBulkSecretDTO = {
|
|||||||
}>;
|
}>;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export enum SecretsOrderBy {
|
||||||
|
Name = "name" // "key" for secrets but using name for use across resources
|
||||||
|
}
|
||||||
|
|
||||||
export type TGetSecretsRawDTO = {
|
export type TGetSecretsRawDTO = {
|
||||||
expandSecretReferences?: boolean;
|
expandSecretReferences?: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
@ -150,6 +180,11 @@ export type TGetSecretsRawDTO = {
|
|||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretRawDTO = {
|
export type TGetASecretRawDTO = {
|
||||||
|
@ -33,7 +33,8 @@ export enum SmtpTemplates {
|
|||||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||||
PkiExpirationAlert = "pkiExpirationAlert.handlebars"
|
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
|
||||||
|
IntegrationSyncFailed = "integrationSyncFailed.handlebars"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Integration Sync Failed</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{{count}} integration(s) failed to sync.</p>
|
||||||
|
<a href="{{integrationUrl}}">
|
||||||
|
View your project integrations.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
<p><strong>Project</strong>: {{projectName}}</p>
|
||||||
|
<p><strong>Environment</strong>: {{environment}}</p>
|
||||||
|
<p><strong>Secret Path</strong>: {{secretPath}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if syncMessage}}
|
||||||
|
<p><b>Reason: </b>{{syncMessage}}</p>
|
||||||
|
{{/if}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -43,14 +44,26 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
|
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
|
||||||
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
silent, err := cmd.Flags().GetBool("silent")
|
silent, err := cmd.Flags().GetBool("silent")
|
||||||
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
|
||||||
|
|
||||||
if !util.IsRunningInDocker() && !silent {
|
if !util.IsRunningInDocker() && !silent {
|
||||||
util.CheckForUpdate()
|
util.CheckForUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||||
|
|
||||||
|
if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
|
||||||
|
if err == nil && token != nil {
|
||||||
|
util.PrintWarning(fmt.Sprintf("Your logged-in session is being overwritten by the token provided from the %s.", token.Source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
|
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
|
||||||
|
@ -160,7 +160,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token == nil) {
|
if token == nil {
|
||||||
util.RequireLocalWorkspaceFile()
|
util.RequireLocalWorkspaceFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ type DynamicSecretLease struct {
|
|||||||
type TokenDetails struct {
|
type TokenDetails struct {
|
||||||
Type string
|
Type string
|
||||||
Token string
|
Token string
|
||||||
|
Source string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SingleFolder struct {
|
type SingleFolder struct {
|
||||||
|
@ -87,11 +87,15 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var source = "--token flag"
|
||||||
|
|
||||||
if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
|
if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
|
||||||
infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
|
infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
|
||||||
|
source = fmt.Sprintf("%s environment variable", INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
|
||||||
|
|
||||||
if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
|
if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
|
||||||
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
||||||
|
source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,12 +107,14 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
|
|||||||
return &models.TokenDetails{
|
return &models.TokenDetails{
|
||||||
Type: SERVICE_TOKEN_IDENTIFIER,
|
Type: SERVICE_TOKEN_IDENTIFIER,
|
||||||
Token: infisicalToken,
|
Token: infisicalToken,
|
||||||
|
Source: source,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.TokenDetails{
|
return &models.TokenDetails{
|
||||||
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
|
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
|
||||||
Token: infisicalToken,
|
Token: infisicalToken,
|
||||||
|
Source: source,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
title: "Export"
|
title: "Export"
|
||||||
openapi: "GET /api/v1/workspace/{workspaceId}/audit-logs"
|
openapi: "GET /api/v1/organization/audit-logs"
|
||||||
---
|
---
|
||||||
|
4
docs/api-reference/endpoints/groups/add-group-user.mdx
Normal file
4
docs/api-reference/endpoints/groups/add-group-user.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Add Group User"
|
||||||
|
openapi: "POST /api/v1/groups/{id}/users/{username}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/groups/create.mdx
Normal file
4
docs/api-reference/endpoints/groups/create.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/groups"
|
||||||
|
---
|
4
docs/api-reference/endpoints/groups/delete.mdx
Normal file
4
docs/api-reference/endpoints/groups/delete.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/groups/{id}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/groups/get-by-id.mdx
Normal file
4
docs/api-reference/endpoints/groups/get-by-id.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get By ID"
|
||||||
|
openapi: "GET /api/v1/groups/{id}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/groups/get.mdx
Normal file
4
docs/api-reference/endpoints/groups/get.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Groups in Organization"
|
||||||
|
openapi: "GET /api/v1/groups"
|
||||||
|
---
|
4
docs/api-reference/endpoints/groups/list-group-users.mdx
Normal file
4
docs/api-reference/endpoints/groups/list-group-users.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Group Users"
|
||||||
|
openapi: "GET /api/v1/groups/{id}/users"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Remove Group User"
|
||||||
|
openapi: "DELETE /api/v1/groups/{id}/users/{username}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/groups/update.mdx
Normal file
4
docs/api-reference/endpoints/groups/update.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/groups/{id}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/project-groups/create.mdx
Normal file
4
docs/api-reference/endpoints/project-groups/create.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create Project Membership"
|
||||||
|
openapi: "POST /api/v2/workspace/{projectId}/groups/{groupId}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/project-groups/delete.mdx
Normal file
4
docs/api-reference/endpoints/project-groups/delete.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete Project Membership"
|
||||||
|
openapi: "DELETE /api/v2/workspace/{projectId}/groups/{groupId}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Project Membership"
|
||||||
|
openapi: "GET /api/v2/workspace/{projectId}/groups/{groupId}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/project-groups/list.mdx
Normal file
4
docs/api-reference/endpoints/project-groups/list.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Project Memberships"
|
||||||
|
openapi: "GET /api/v2/workspace/{projectId}/groups"
|
||||||
|
---
|
4
docs/api-reference/endpoints/project-groups/update.mdx
Normal file
4
docs/api-reference/endpoints/project-groups/update.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update Project Membership"
|
||||||
|
openapi: "PATCH /api/v2/workspace/{projectId}/groups/{groupId}"
|
||||||
|
---
|
@ -51,7 +51,6 @@ infisical export --template=<path to template>
|
|||||||
<Info>
|
<Info>
|
||||||
Alternatively, you may use service tokens.
|
Alternatively, you may use service tokens.
|
||||||
|
|
||||||
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
export INFISICAL_TOKEN=<service-token>
|
export INFISICAL_TOKEN=<service-token>
|
||||||
|
@ -54,8 +54,6 @@ $ infisical run -- npm run dev
|
|||||||
<Info>
|
<Info>
|
||||||
Alternatively, you may use service tokens.
|
Alternatively, you may use service tokens.
|
||||||
|
|
||||||
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
export INFISICAL_TOKEN=<service-token>
|
export INFISICAL_TOKEN=<service-token>
|
||||||
|
@ -33,7 +33,6 @@ $ infisical secrets
|
|||||||
<Info>
|
<Info>
|
||||||
Alternatively, you may use service tokens.
|
Alternatively, you may use service tokens.
|
||||||
|
|
||||||
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
export INFISICAL_TOKEN=<service-token>
|
export INFISICAL_TOKEN=<service-token>
|
||||||
|
@ -206,8 +206,6 @@ infisical <any-command> --domain="https://your-self-hosted-infisical.com/api"
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="Can I use the CLI with service tokens?">
|
<Accordion title="Can I use the CLI with service tokens?">
|
||||||
Yes. Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
|
||||||
|
|
||||||
To use Infisical for non local development scenarios, please create a service token. The service token will allow you to authenticate and interact with Infisical. Once you have created a service token with the required permissions, you’ll need to feed the token to the CLI.
|
To use Infisical for non local development scenarios, please create a service token. The service token will allow you to authenticate and interact with Infisical. Once you have created a service token with the required permissions, you’ll need to feed the token to the CLI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
164
docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx
Normal file
164
docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
title: "Azure Entra Id"
|
||||||
|
description: "Learn how to dynamically generate Azure Entra Id user credentials."
|
||||||
|
---
|
||||||
|
|
||||||
|
The Infisical Azure Entra Id dynamic secret allows you to generate Azure Entra Id credentials on demand based on configured role.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
Login to [Microsoft Entra ID](https://entra.microsoft.com/)
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Go to Overview, Copy and store `Tenant Id`
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Go to Applications > App registrations. Click on New Registration.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Enter an application name. Click Register.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Copy and store `Application Id`.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Go to Clients and Secrets. Click on New Client Secret.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Enter a description, select expiry and click Add.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Copy and store `Client Secret` value.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Go to API Permissions. Click on Add a permission.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Click on Microsoft Graph.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Click on Application Permissions. Search and select `User.ReadWrite.All` and click Add permissions.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Click on Grant admin consent for app. Click yes to confirm.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Go to Dashboard. Click on show more.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Click on Roles & admins. Search for User Administrator and click on it.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
Click on Add assignments. Search for the application name you created and select it. Click on Add.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Set up Dynamic Secrets with Azure Entra ID
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open Secret Overview Dashboard">
|
||||||
|
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select 'Azure Entra ID'">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|
<ParamField path="Secret Prefix" type="string" required>
|
||||||
|
Prefix for the secrets to be created
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Default TTL" type="string" required>
|
||||||
|
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Max TTL" type="string" required>
|
||||||
|
Maximum time-to-live for a generated secret.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Tenant ID" type="string" required>
|
||||||
|
The Tenant ID of your Azure Entra ID account.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Application ID" type="string" required>
|
||||||
|
The Application ID of the application you created in Azure Entra ID.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Client Secret" type="string" required>
|
||||||
|
The Client Secret of the application you created in Azure Entra ID.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Users" type="selection" required>
|
||||||
|
Multi select list of users to generate secrets for.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Click `Submit`">
|
||||||
|
After submitting the form, you will see a dynamic secrets for each user created in the dashboard.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||||
|
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||||
|
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Audit or Revoke Leases
|
||||||
|
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||||
|
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Renew Leases
|
||||||
|
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||||
|
</Warning>
|
@ -17,3 +17,40 @@ Check the box in Personal Settings > Two-factor Authentication to enable email-b
|
|||||||
building support for other forms of identification via SMS and Authenticator
|
building support for other forms of identification via SMS and Authenticator
|
||||||
App.
|
App.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
## Entra ID / Azure AD MFA
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Before proceeding make sure you've enabled [SAML SSO for Entra ID / Azure AD](./sso/azure).
|
||||||
|
|
||||||
|
We also encourage you to have your team download and setup the
|
||||||
|
[Microsoft Authenticator App](https://www.microsoft.com/en-us/security/mobile-authenticator-app) prior to enabling MFA.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open your Infisical Application in the Microsoft Entra Admin Center">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Tap on Conditional Access under the Security Tab">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Tap on Create New Policy from Templates">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select Require MFA for All Users and Tap on Review + Create">
|
||||||
|

|
||||||
|
<Note>
|
||||||
|
By default all users except the configuring admin will be setup to require MFA.
|
||||||
|
Microsoft encourages keeping at least one admin excluded from MFA to prevent accidental lockout.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
<Step title="Set Policy State to Enabled and Tap on Create">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="MFA is now Required When Accessing Infisical">
|
||||||
|

|
||||||
|
<Note>
|
||||||
|
If users have not setup MFA for Entra / Azure they will be prompted to do so at this time.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
@ -116,3 +116,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
|
|||||||
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`.
|
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`.
|
||||||
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
|
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you'd like to require Multi-factor Authentication for your team members to access Infisical check out our [Entra ID / Azure AD MFA](../mfa#entra-id-azure-ad-mfa) guide.
|
||||||
|
</Note>
|
||||||
|
@ -3,13 +3,6 @@ title: "Service Token"
|
|||||||
description: "Infisical service tokens allow users to programmatically interact with Infisical."
|
description: "Infisical service tokens allow users to programmatically interact with Infisical."
|
||||||
---
|
---
|
||||||
|
|
||||||
<Warning>
|
|
||||||
Service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities).
|
|
||||||
|
|
||||||
They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
|
||||||
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
Service tokens are authentication credentials that services can use to access designated endpoints in the Infisical API to manage project resources like secrets.
|
Service tokens are authentication credentials that services can use to access designated endpoints in the Infisical API to manage project resources like secrets.
|
||||||
Each service token can be provisioned scoped access to select environment(s) and path(s) within them.
|
Each service token can be provisioned scoped access to select environment(s) and path(s) within them.
|
||||||
|
|
||||||
|
@ -5,9 +5,11 @@ description: "Learn how to setup Slack integration"
|
|||||||
|
|
||||||
This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
|
This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
|
||||||
|
|
||||||
|
## Setting up Slack integration in your projects
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Infisical Cloud">
|
<Tab title="Infisical Cloud">
|
||||||
## Create Slack workflow integration
|
### Create Slack workflow integration
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
|
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
|
||||||
In order to use Slack integration in your projects, you will first have to
|
In order to use Slack integration in your projects, you will first have to
|
||||||
@ -32,7 +34,7 @@ This guide will provide step by step instructions on how to configure Slack inte
|
|||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Configure project to use Slack workflow integration
|
### Configure project to use Slack workflow integration
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||||
@ -56,7 +58,7 @@ This guide will provide step by step instructions on how to configure Slack inte
|
|||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Self-hosted setup">
|
<Tab title="Self-hosted setup">
|
||||||
## Configure admin settings
|
### Configure admin settings
|
||||||
Note that this step only has to be done once for the entire instance.
|
Note that this step only has to be done once for the entire instance.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
@ -90,7 +92,7 @@ This guide will provide step by step instructions on how to configure Slack inte
|
|||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Create Slack workflow integration
|
### Create Slack workflow integration
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
|
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
|
||||||
@ -116,7 +118,7 @@ This guide will provide step by step instructions on how to configure Slack inte
|
|||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Configure project to use Slack workflow integration
|
### Configure project to use Slack workflow integration
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||||
@ -140,3 +142,23 @@ This guide will provide step by step instructions on how to configure Slack inte
|
|||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
## Using the Slack integration in your private channels
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="In the Apps section on Slack, find the Infisical app and view the app details">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select Add this app to a channel">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Find the private channel you want to setup notifications for and press Add">
|
||||||
|

|
||||||
|
You can now view the private channels in the Slack channel selection fields!
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user