mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-10 07:25:40 +00:00
Compare commits
238 Commits
gateway-ar
...
secret-rot
Author | SHA1 | Date | |
---|---|---|---|
bcf86aea90 | |||
06024065cd | |||
2d207147a6 | |||
4c04d0f871 | |||
484a9b28ef | |||
94f79ade4a | |||
5a63a6dbe4 | |||
add97c9b38 | |||
768ba4f4dc | |||
18c32d872c | |||
1fd40ab6ab | |||
9d258f57ce | |||
45ccbaf4c9 | |||
8de7261c9a | |||
67b1b79fe3 | |||
31477f4d2b | |||
f200372d74 | |||
f955b68519 | |||
9269b63943 | |||
8f96653273 | |||
7dffc08eba | |||
126b0ce7e7 | |||
0b71f7f297 | |||
e53439d586 | |||
c86e508817 | |||
6426b85c1e | |||
3d6da1e548 | |||
7e46fe8148 | |||
3756a1901d | |||
9c8adf75ec | |||
f461eaa432 | |||
a1fbc140ee | |||
ea27870ce3 | |||
48943b4d78 | |||
fd1afc2cbe | |||
6905029455 | |||
e89fb33981 | |||
5ebf142e3e | |||
94d7d2b029 | |||
e39d1a0530 | |||
4c5f3859d6 | |||
16866d46bf | |||
4f4764dfcd | |||
bdceea4c91 | |||
32fa6866e4 | |||
b4faef797c | |||
08732cab62 | |||
81d5f639ae | |||
25b83d4b86 | |||
155e59e571 | |||
8fbd3f2fce | |||
a500f00a49 | |||
6842f7aa8b | |||
ad207786e2 | |||
ace8c37c25 | |||
f15e61dbd9 | |||
4c82408b51 | |||
8146dcef16 | |||
2e90addbc5 | |||
427201a634 | |||
0b55ac141c | |||
aecfa268ae | |||
fdfc020efc | |||
62aa80a104 | |||
cf9d8035bd | |||
d0c9f1ca53 | |||
2ecc7424d9 | |||
c04b97c689 | |||
7600a86dfc | |||
8924eaf251 | |||
82e9504285 | |||
c4e10df754 | |||
ce60e96008 | |||
930b59cb4f | |||
ec363a5ad4 | |||
c0de4ae3ee | |||
de7e92ccfc | |||
522d81ae1a | |||
ef22b39421 | |||
02153ffb32 | |||
1d14cdf334 | |||
39b323dd9c | |||
b0b55344ce | |||
d9d62384e7 | |||
76f34501dc | |||
7415bb93b8 | |||
7a1c08a7f2 | |||
568aadef75 | |||
84f9eb5f9f | |||
87ac723fcb | |||
a6dab47552 | |||
79d8a9debb | |||
08bac83bcc | |||
46c90f03f0 | |||
d7722f7587 | |||
a42bcb3393 | |||
192dba04a5 | |||
0cc3240956 | |||
667580546b | |||
9fd662b7f7 | |||
a56cbbc02f | |||
dc30465afb | |||
f1caab2d00 | |||
1d186b1950 | |||
9cf5908cc1 | |||
f1b6c3764f | |||
4e6c860c69 | |||
eda9ed257e | |||
38cf43176e | |||
f5c7943f2f | |||
3c59f7f350 | |||
84cc7bcd6c | |||
159c27ac67 | |||
de5a432745 | |||
387780aa94 | |||
3887ce800b | |||
1a06b3e1f5 | |||
5f0dd31334 | |||
7e14c58931 | |||
627e17b3ae | |||
39b7a4a111 | |||
e7c512999e | |||
c19016e6e6 | |||
20477ce2b0 | |||
e04b2220be | |||
edf6a37fe5 | |||
f5749e326a | |||
75e0a68b68 | |||
71b8e3dbce | |||
4dc56033b1 | |||
ed37b99756 | |||
6fa41a609b | |||
e46f10292c | |||
acb22cdf36 | |||
c9da8477c8 | |||
5e4b478b74 | |||
765be2d99d | |||
719a18c218 | |||
16d3bbb67a | |||
872a3fe48d | |||
c7414e00f9 | |||
ad1dd55b8b | |||
497761a0e5 | |||
483fb458dd | |||
17cf602a65 | |||
23f6f5dfd4 | |||
b9b76579ac | |||
761965696b | |||
ace2500885 | |||
4eff7d8ea5 | |||
c4512ae111 | |||
78c349c09a | |||
09df440613 | |||
a8fc0e540a | |||
46ce46b5a0 | |||
dc88115d43 | |||
955657e172 | |||
f1ba64aa66 | |||
d74197aeb4 | |||
97567d06d4 | |||
3986df8e8a | |||
3fcd84b592 | |||
29e39b558b | |||
9458c8b04f | |||
3b95c5d859 | |||
de8f315211 | |||
9960d58e1b | |||
0057404562 | |||
47ca1b3011 | |||
716cd090c4 | |||
e870bb3ade | |||
98c9e98082 | |||
a814f459ab | |||
66817a40db | |||
20bd2ca71c | |||
004a8b71a2 | |||
f0fce3086e | |||
a9e7db6fc0 | |||
2bd681d58f | |||
51fef3ce60 | |||
df9e7bf6ee | |||
04479bb70a | |||
cdc90411e5 | |||
dcb05a3093 | |||
b055cda64d | |||
f68602280e | |||
f9483afe95 | |||
d742534f6a | |||
99eb8eb8ed | |||
1dea024880 | |||
699e03c1a9 | |||
f6372249b4 | |||
0f42fcd688 | |||
2e02f8bea8 | |||
8203158c63 | |||
ada04ed4fc | |||
cc9cc70125 | |||
045debeaf3 | |||
3fb8ad2fac | |||
795d9e4413 | |||
67f2e4671a | |||
cbe3acde74 | |||
de480b5771 | |||
07b93c5cec | |||
77431b4719 | |||
50610945be | |||
57f54440d6 | |||
9711e73a06 | |||
214f837041 | |||
58ebebb162 | |||
65ddddb6de | |||
a55b26164a | |||
6cd448b8a5 | |||
c48c9ae628 | |||
7003ad608a | |||
104edca6f1 | |||
abc2ffca57 | |||
b7640f2d03 | |||
2ee4d68fd0 | |||
3ca931acf1 | |||
7f6715643d | |||
8e311658d4 | |||
9116acd37b | |||
0513307d98 | |||
28c2f1874e | |||
efc3b6d474 | |||
07e1d1b130 | |||
7f76779124 | |||
30bcf1f204 | |||
cd5b6da541 | |||
2dda7180a9 | |||
30ccfbfc8e | |||
c54eafc128 | |||
757942aefc | |||
1d57629036 | |||
8061066e27 | |||
c993b1bbe3 | |||
2cbf33ac14 |
3
.envrc
Normal file
3
.envrc
Normal file
@ -0,0 +1,3 @@
|
||||
# Learn more at https://direnv.net
|
||||
# We instruct direnv to use our Nix flake for a consistent development environment.
|
||||
use flake
|
@ -92,7 +92,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
- name: Install openapi-diff
|
||||
run: go install github.com/tufin/oasdiff@latest
|
||||
run: go install github.com/oasdiff/oasdiff@latest
|
||||
- name: Running OpenAPI Spec diff action
|
||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||
- name: cleanup
|
||||
|
8
.github/workflows/run-backend-tests.yml
vendored
8
.github/workflows/run-backend-tests.yml
vendored
@ -34,7 +34,10 @@ jobs:
|
||||
working-directory: backend
|
||||
- name: Start postgres and redis
|
||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||
- name: Start integration test
|
||||
- name: Run unit test
|
||||
run: npm run test:unit
|
||||
working-directory: backend
|
||||
- name: Run integration test
|
||||
run: npm run test:e2e
|
||||
working-directory: backend
|
||||
env:
|
||||
@ -44,4 +47,5 @@ jobs:
|
||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||
- name: cleanup
|
||||
run: |
|
||||
docker compose -f "docker-compose.dev.yml" down
|
||||
docker compose -f "docker-compose.dev.yml" down
|
||||
|
||||
|
@ -120,4 +120,3 @@ export default {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
166
backend/package-lock.json
generated
166
backend/package-lock.json
generated
@ -31,7 +31,7 @@
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
@ -6747,32 +6747,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz",
|
||||
"integrity": "sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.0.1.tgz",
|
||||
"integrity": "sha512-YQzFPEC+CnsfO9AFYnwfYZKIzOLx3kITaC1HrjHVLTo6hxcQhc+LgHODOMvW4VCV95Gwrz1MshRUWCPzkDqmnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/passport": "^1.0.11",
|
||||
"@types/xml-crypto": "^1.4.2",
|
||||
"@types/xml-encryption": "^1.2.1",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"@xmldom/xmldom": "^0.8.6",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/qs": "^6.9.11",
|
||||
"@types/xml-encryption": "^1.2.4",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@xmldom/is-dom-node": "^1.0.1",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"debug": "^4.3.4",
|
||||
"xml-crypto": "^3.0.1",
|
||||
"xml-crypto": "^6.0.1",
|
||||
"xml-encryption": "^3.0.2",
|
||||
"xml2js": "^0.5.0",
|
||||
"xmlbuilder": "^15.1.1"
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"xpath": "^0.0.34"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@ -6783,25 +6786,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"node_modules/@node-saml/node-saml/node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml/node_modules/xml2js/node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/passport-saml": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-4.0.4.tgz",
|
||||
"integrity": "sha512-xFw3gw0yo+K1mzlkW15NeBF7cVpRHN/4vpjmBKzov5YFImCWh/G0LcTZ8krH3yk2/eRPc3Or8LRPudVJBjmYaw==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.0.1.tgz",
|
||||
"integrity": "sha512-fMztg3zfSnjLEgxvpl6HaDMNeh0xeQX4QHiF9e2Lsie2dc4qFE37XYbQZhVmn8XJ2awPpSWLQ736UskYgGU8lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@node-saml/node-saml": "^4.0.4",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/passport": "^1.0.11",
|
||||
"@types/passport-strategy": "^0.2.35",
|
||||
"passport": "^0.6.0",
|
||||
"@node-saml/node-saml": "^5.0.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@types/passport-strategy": "^0.2.38",
|
||||
"passport": "^0.7.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
@ -9606,6 +9627,7 @@
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
@ -9725,9 +9747,10 @@
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.9.5",
|
||||
@ -9907,9 +9930,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
|
||||
"integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw=="
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
@ -10058,19 +10082,11 @@
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/xml-crypto": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.6.tgz",
|
||||
"integrity": "sha512-A6jEW2FxLZo1CXsRWnZHUX2wzR3uDju2Bozt6rDbSmU/W8gkilaVbwFEVN0/NhnUdMVzwYobWtM6bU1QJJFb7Q==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"xpath": "0.0.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/xml-encryption": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz",
|
||||
"integrity": "sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@ -10079,6 +10095,7 @@
|
||||
"version": "0.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
|
||||
"integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@ -10522,10 +10539,20 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/is-dom-node": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz",
|
||||
"integrity": "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
@ -18222,9 +18249,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
|
||||
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
@ -23692,42 +23720,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xml-crypto": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.0.tgz",
|
||||
"integrity": "sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz",
|
||||
"integrity": "sha512-v05aU7NS03z4jlZ0iZGRFeZsuKO1UfEbbYiaeRMiATBFs6Jq9+wqKquEMTn4UTrYZ9iGD8yz3KT4L9o2iF682w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.8",
|
||||
"xpath": "0.0.32"
|
||||
"@xmldom/is-dom-node": "^1.0.1",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"xpath": "^0.0.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-crypto/node_modules/xpath": {
|
||||
"version": "0.0.32",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
|
||||
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==",
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz",
|
||||
"integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-encryption": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.0.2.tgz",
|
||||
"integrity": "sha512-VxYXPvsWB01/aqVLd6ZMPWZ+qaj0aIdF+cStrVJMcFj3iymwZeI0ABzB3VqMYv48DkSpRhnrXqTUkR34j+UDyg==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.1.0.tgz",
|
||||
"integrity": "sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.5",
|
||||
"escape-html": "^1.0.3",
|
||||
"xpath": "0.0.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-encryption/node_modules/xpath": {
|
||||
"version": "0.0.32",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
|
||||
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
@ -23764,6 +23794,7 @@
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
@ -23774,9 +23805,10 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
|
||||
},
|
||||
"node_modules/xpath": {
|
||||
"version": "0.0.27",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",
|
||||
"integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==",
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz",
|
||||
"integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
|
@ -40,6 +40,7 @@
|
||||
"type:check": "tsc --noEmit",
|
||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||
@ -70,6 +71,7 @@
|
||||
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||
},
|
||||
"keywords": [],
|
||||
@ -146,7 +148,7 @@
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -33,6 +33,7 @@ import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||
@ -230,6 +231,7 @@ declare module "fastify" {
|
||||
kmip: TKmipServiceFactory;
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
10
backend/src/@types/knex.d.ts
vendored
10
backend/src/@types/knex.d.ts
vendored
@ -393,6 +393,11 @@ import {
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate
|
||||
} from "@app/db/schemas/external-group-org-role-mappings";
|
||||
import {
|
||||
TSecretRotationsV2,
|
||||
TSecretRotationsV2Insert,
|
||||
TSecretRotationsV2Update
|
||||
} from "@app/db/schemas/secret-rotations-v2";
|
||||
import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs";
|
||||
import {
|
||||
TSecretV2TagJunction,
|
||||
@ -950,5 +955,10 @@ declare module "knex/types/tables" {
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate
|
||||
>;
|
||||
[TableName.SecretRotationV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretRotationsV2,
|
||||
TSecretRotationsV2Insert,
|
||||
TSecretRotationsV2Update
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
// not setting a foreign constraint so that cascade effects are not triggered
|
||||
if (!doesGatewayColExist) {
|
||||
|
@ -0,0 +1,45 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||
if (!hasSecretVersionV2UserActorId) {
|
||||
t.uuid("userActorId");
|
||||
t.foreign("userActorId").references("id").inTable(TableName.Users);
|
||||
}
|
||||
if (!hasSecretVersionV2IdentityActorId) {
|
||||
t.uuid("identityActorId");
|
||||
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
|
||||
}
|
||||
if (!hasSecretVersionV2ActorType) {
|
||||
t.string("actorType");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||
if (hasSecretVersionV2UserActorId) {
|
||||
t.dropColumn("userActorId");
|
||||
}
|
||||
if (hasSecretVersionV2IdentityActorId) {
|
||||
t.dropColumn("identityActorId");
|
||||
}
|
||||
if (hasSecretVersionV2ActorType) {
|
||||
t.dropColumn("actorType");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Organization)) {
|
||||
const hasSecretShareToAnyoneCol = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"allowSecretSharingOutsideOrganization"
|
||||
);
|
||||
|
||||
if (!hasSecretShareToAnyoneCol) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.boolean("allowSecretSharingOutsideOrganization").defaultTo(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Organization)) {
|
||||
const hasSecretShareToAnyoneCol = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"allowSecretSharingOutsideOrganization"
|
||||
);
|
||||
if (hasSecretShareToAnyoneCol) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("allowSecretSharingOutsideOrganization");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManaged"))) {
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.boolean("isPlatformManaged").defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManaged")) {
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.dropColumn("isPlatformManaged");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.SecretRotationV2))) {
|
||||
await knex.schema.createTable(TableName.SecretRotationV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name", 32).notNullable();
|
||||
t.string("description");
|
||||
t.string("type").notNullable();
|
||||
t.integer("interval").notNullable();
|
||||
t.jsonb("parameters").notNullable();
|
||||
t.binary("encryptedGeneratedCredentials").notNullable();
|
||||
t.boolean("isAutoRotationEnabled").notNullable().defaultTo(true);
|
||||
t.integer("activeIndex").notNullable().defaultTo(0);
|
||||
// we're including projectId in addition to folder ID because we allow folderId to be null (if the folder
|
||||
// is deleted), to preserve configuration
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("folderId");
|
||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("SET NULL");
|
||||
t.uuid("connectionId").notNullable();
|
||||
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.timestamps(true, true, true);
|
||||
t.string("rotationStatus");
|
||||
t.string("lastRotationJobId");
|
||||
t.string("lastRotationMessage", 1024);
|
||||
t.datetime("lastRotatedAt");
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.SecretRotationV2);
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretRotationV2, (t) => {
|
||||
t.unique(["projectId", "name"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretRotationV2);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretRotationV2);
|
||||
}
|
@ -19,7 +19,8 @@ export const AppConnectionsSchema = z.object({
|
||||
version: z.number().default(1),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isPlatformManaged: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||
|
@ -140,7 +140,8 @@ export enum TableName {
|
||||
KmipClient = "kmip_clients",
|
||||
KmipOrgConfig = "kmip_org_configs",
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
KmipClientCertificates = "kmip_client_certificates"
|
||||
KmipClientCertificates = "kmip_client_certificates",
|
||||
SecretRotationV2 = "secret_rotations_v2"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
@ -22,7 +22,8 @@ export const OrganizationsSchema = z.object({
|
||||
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional()
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
35
backend/src/db/schemas/secret-rotations-v2.ts
Normal file
35
backend/src/db/schemas/secret-rotations-v2.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretRotationsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
interval: z.number(),
|
||||
parameters: z.unknown(),
|
||||
encryptedGeneratedCredentials: zodBuffer,
|
||||
isAutoRotationEnabled: z.boolean().default(true),
|
||||
activeIndex: z.number().default(0),
|
||||
projectId: z.string(),
|
||||
folderId: z.string().uuid().nullable().optional(),
|
||||
connectionId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
rotationStatus: z.string().nullable().optional(),
|
||||
lastRotationJobId: z.string().nullable().optional(),
|
||||
lastRotationMessage: z.string().nullable().optional(),
|
||||
lastRotatedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretRotationsV2 = z.infer<typeof SecretRotationsV2Schema>;
|
||||
export type TSecretRotationsV2Insert = Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretRotationsV2Update = Partial<Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>>;
|
@ -12,7 +12,6 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedValue: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
hashedHex: z.string().nullable().optional(),
|
||||
@ -27,7 +26,8 @@ export const SecretSharingSchema = z.object({
|
||||
lastViewedAt: z.date().nullable().optional(),
|
||||
password: z.string().nullable().optional(),
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional()
|
||||
identifier: z.string().nullable().optional(),
|
||||
type: z.string().default("share")
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
|
||||
folderId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
userActorId: z.string().uuid().nullable().optional(),
|
||||
identityActorId: z.string().uuid().nullable().optional(),
|
||||
actorType: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
|
@ -1,4 +1,3 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
@ -6,6 +5,7 @@ import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/pro
|
||||
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
|
@ -1,11 +1,11 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
@ -1,10 +1,10 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { KmipClientsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
|
||||
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
|
@ -25,7 +25,7 @@ type TSAMLConfig = {
|
||||
callbackUrl: string;
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
cert: string;
|
||||
idpCert: string;
|
||||
audience: string;
|
||||
wantAuthnResponseSigned?: boolean;
|
||||
wantAssertionsSigned?: boolean;
|
||||
@ -72,7 +72,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
|
||||
entryPoint: ssoConfig.entryPoint,
|
||||
issuer: ssoConfig.issuer,
|
||||
cert: ssoConfig.cert,
|
||||
idpCert: ssoConfig.cert,
|
||||
audience: appCfg.SITE_URL || ""
|
||||
};
|
||||
if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) {
|
||||
@ -302,15 +302,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const saml = await server.services.saml.createSamlCfg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId,
|
||||
...req.body
|
||||
const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
|
||||
const { permission } = req;
|
||||
|
||||
return server.services.saml.createSamlCfg({
|
||||
isActive,
|
||||
authProvider,
|
||||
issuer,
|
||||
entryPoint,
|
||||
idpCert: cert,
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
actorAuthMethod: permission.authMethod,
|
||||
actorOrgId: permission.orgId,
|
||||
orgId: req.body.organizationId
|
||||
});
|
||||
return saml;
|
||||
}
|
||||
});
|
||||
|
||||
@ -337,15 +343,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const saml = await server.services.saml.updateSamlCfg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId,
|
||||
...req.body
|
||||
const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
|
||||
const { permission } = req;
|
||||
|
||||
return server.services.saml.updateSamlCfg({
|
||||
isActive,
|
||||
authProvider,
|
||||
issuer,
|
||||
entryPoint,
|
||||
idpCert: cert,
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
actorAuthMethod: permission.authMethod,
|
||||
actorOrgId: permission.orgId,
|
||||
orgId: req.body.organizationId
|
||||
});
|
||||
return saml;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,16 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
SecretApprovalRequestsReviewersSchema,
|
||||
SecretApprovalRequestsSchema,
|
||||
SecretTagsSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
|
||||
@ -250,14 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
}
|
||||
});
|
||||
|
||||
const tagSchema = SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.array()
|
||||
.optional();
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
@ -291,7 +278,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
||||
.extend({
|
||||
op: z.string(),
|
||||
tags: tagSchema,
|
||||
tags: SanitizedTagSchema.array().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.nullish(),
|
||||
secret: z
|
||||
.object({
|
||||
@ -310,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string().optional(),
|
||||
secretComment: z.string().optional(),
|
||||
tags: tagSchema,
|
||||
tags: SanitizedTagSchema.array().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.nullish()
|
||||
})
|
||||
.optional()
|
||||
|
@ -1,6 +1,6 @@
|
||||
import z from "zod";
|
||||
|
||||
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||
import { RAW_SECRETS } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const AccessListEntrySchema = z
|
||||
.object({
|
||||
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
|
||||
allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(),
|
||||
id: z.string(),
|
||||
membershipId: z.string(),
|
||||
name: z.string()
|
||||
|
@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretVersions: secretRawSchema.array()
|
||||
secretVersions: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
||||
offset: req.query.offset,
|
||||
secretId: req.params.secretId
|
||||
});
|
||||
|
||||
return { secretVersions };
|
||||
}
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
|
||||
import { SecretSnapshotsSchema } from "@app/db/schemas";
|
||||
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 { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
secretVersions: secretRawSchema
|
||||
.omit({ _id: true, environment: true, workspace: true, type: true })
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretId: z.string(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
}).array()
|
||||
tags: SanitizedTagSchema.array()
|
||||
})
|
||||
.array(),
|
||||
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
||||
@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.secretSnapshotId
|
||||
});
|
||||
|
||||
return { secretSnapshot };
|
||||
}
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
@ -1,5 +1,4 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
@ -10,6 +9,7 @@ import {
|
||||
isValidUserPattern
|
||||
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
|
||||
import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
@ -1,10 +1,11 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
||||
body: z.object({
|
||||
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
.refine(checkForInvalidPermissionCombination),
|
||||
type: z.discriminatedUnion("isTemporary", [
|
||||
z.object({
|
||||
isTemporary: z.literal(false)
|
||||
@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
||||
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.optional()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions)
|
||||
.refine(checkForInvalidPermissionCombination),
|
||||
type: z.discriminatedUnion("isTemporary", [
|
||||
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
|
||||
z.object({
|
||||
|
@ -1,10 +1,11 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
||||
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission)
|
||||
.refine(checkForInvalidPermissionCombination),
|
||||
type: z.discriminatedUnion("isTemporary", [
|
||||
z.object({
|
||||
isTemporary: z.literal(false)
|
||||
@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission)
|
||||
.refine(checkForInvalidPermissionCombination),
|
||||
type: z.discriminatedUnion("isTemporary", [
|
||||
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
|
||||
z.object({
|
||||
|
@ -1,3 +1,8 @@
|
||||
import {
|
||||
registerSecretRotationV2Router,
|
||||
SECRET_ROTATION_REGISTER_ROUTER_MAP
|
||||
} from "@app/ee/routes/v2/secret-rotation-v2-routers";
|
||||
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
|
||||
@ -13,4 +18,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerIdentityProjectAdditionalPrivilegeRouter, {
|
||||
prefix: "/identity-project-additional-privilege"
|
||||
});
|
||||
|
||||
await server.register(
|
||||
async (secretRotationV2Router) => {
|
||||
// register generic secret sync endpoints
|
||||
await secretRotationV2Router.register(registerSecretRotationV2Router);
|
||||
|
||||
// register service specific secret rotation endpoints (secret-rotations/postgres-credentials, etc.)
|
||||
for await (const [type, router] of Object.entries(SECRET_ROTATION_REGISTER_ROUTER_MAP)) {
|
||||
await secretRotationV2Router.register(router, { prefix: `/${type}` });
|
||||
}
|
||||
},
|
||||
{ prefix: "/secret-rotations" }
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -37,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(PROJECT_ROLE.CREATE.slug),
|
||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.describe(PROJECT_ROLE.CREATE.permissions)
|
||||
.refine(checkForInvalidPermissionCombination)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -92,7 +95,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.describe(PROJECT_ROLE.UPDATE.permissions)
|
||||
.optional()
|
||||
.superRefine(checkForInvalidPermissionCombination)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
12
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal file
12
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
|
||||
export * from "./secret-rotation-v2-router";
|
||||
|
||||
export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
SecretRotation,
|
||||
(server: FastifyZodProvider) => Promise<void>
|
||||
> = {
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreatePostgresCredentialsRotationSchema,
|
||||
PostgresCredentialsRotationSchema,
|
||||
UpdatePostgresCredentialsRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerPostgresCredentialsRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.PostgresCredentials,
|
||||
server,
|
||||
responseSchema: PostgresCredentialsRotationSchema,
|
||||
createSchema: CreatePostgresCredentialsRotationSchema,
|
||||
updateSchema: UpdatePostgresCredentialsRotationSchema
|
||||
});
|
@ -0,0 +1,402 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import {
|
||||
TSecretRotationV2,
|
||||
TSecretRotationV2Input
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { startsWithVowel } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretRotationEndpoints = <T extends TSecretRotationV2, I extends TSecretRotationV2Input>({
|
||||
server,
|
||||
type,
|
||||
createSchema,
|
||||
updateSchema,
|
||||
responseSchema
|
||||
}: {
|
||||
type: SecretRotation;
|
||||
server: FastifyZodProvider;
|
||||
createSchema: z.ZodType<{
|
||||
name: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
projectId: string;
|
||||
connectionId: string;
|
||||
parameters: I["parameters"];
|
||||
description?: string | null;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
interval: number;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
connectionId?: string;
|
||||
name?: string;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
parameters?: I["parameters"];
|
||||
description?: string | null;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
interval?: number;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
const rotationType = SECRET_ROTATION_NAME_MAP[type];
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `List the ${rotationType} Rotations for the specified project.`,
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST(type).projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotations: responseSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId }
|
||||
} = req;
|
||||
|
||||
const secretRotations = (await server.services.secretRotationV2.listSecretRotationsByProjectId(
|
||||
{ projectId, type },
|
||||
req.permission
|
||||
)) as T[];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATIONS,
|
||||
metadata: {
|
||||
type,
|
||||
count: secretRotations.length,
|
||||
rotationIds: secretRotations.map((rotation) => rotation.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotations };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:rotationId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the specified ${rotationType} Rotation by ID.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.GET_BY_ID(type).rotationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.findSecretRotationById(
|
||||
{ rotationId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: secretRotation.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId,
|
||||
type
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/rotation-name/:rotationName`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the specified ${rotationType} Rotation by name and project ID.`,
|
||||
params: z.object({
|
||||
rotationName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Rotation name required")
|
||||
.describe(SecretRotations.GET_BY_NAME(type).rotationName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.GET_BY_NAME(type).projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationName } = req.params;
|
||||
const { projectId } = req.query;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.findSecretRotationByName(
|
||||
{ rotationName, projectId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId: secretRotation.id,
|
||||
type
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Create ${
|
||||
startsWithVowel(rotationType) ? "an" : "a"
|
||||
} ${rotationType} Rotation for the specified project.`,
|
||||
body: createSchema,
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const secretRotation = (await server.services.secretRotationV2.createSecretRotation(
|
||||
{ ...req.body, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: secretRotation.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId: secretRotation.id,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:rotationId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Update the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.UPDATE(type).rotationId)
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.updateSecretRotation(
|
||||
{ ...req.body, rotationId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: secretRotation.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: `/:rotationId`,
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Delete the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.DELETE(type).rotationId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
removeSecrets: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
.describe(SecretRotations.DELETE(type).removeSecrets)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
const { removeSecrets } = req.query;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation(
|
||||
{ type, rotationId, removeSecrets },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
type,
|
||||
rotationId,
|
||||
removeSecrets
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:rotationId/credentials",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the active and inactive credentials for the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.GET_CREDENTIALS_BY_ID(type).rotationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
// TODO!
|
||||
// const secretRotation = (await server.services.secretRotation.triggerSecretRotationRotationSecretsById(
|
||||
// {
|
||||
// rotationId,
|
||||
// type,
|
||||
// auditLogInfo: req.auditLogInfo
|
||||
// },
|
||||
// req.permission
|
||||
// )) as T;
|
||||
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// orgId: req.permission.orgId,
|
||||
// event: {
|
||||
// type: EventType.DELETE_SECRET_ROTATION,
|
||||
// metadata: {
|
||||
// type,
|
||||
// rotationId,
|
||||
// removeSecrets
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
return { secretRotation: null };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:rotationId/rotate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Rotate the credentials for the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.ROTATE(type).rotationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
// TODO!
|
||||
// const secretRotation = (await server.services.secretRotation.triggerSecretRotationRotationSecretsById(
|
||||
// {
|
||||
// rotationId,
|
||||
// type,
|
||||
// auditLogInfo: req.auditLogInfo
|
||||
// },
|
||||
// req.permission
|
||||
// )) as T;
|
||||
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// orgId: req.permission.orgId,
|
||||
// event: {
|
||||
// type: EventType.DELETE_SECRET_ROTATION,
|
||||
// metadata: {
|
||||
// type,
|
||||
// rotationId,
|
||||
// removeSecrets
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
return { secretRotation: null };
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,81 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
PostgresCredentialsRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SecretRotationV2Schema = z.discriminatedUnion("type", [PostgresCredentialsRotationSchema]);
|
||||
|
||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [PostgresCredentialsRotationListItemSchema]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/options",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List the available Secret Rotation Options.",
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRotationOptions: SecretRotationV2OptionsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: () => {
|
||||
const secretRotationOptions = server.services.secretRotationV2.listSecretRotationOptions();
|
||||
return { secretRotationOptions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List all the Secret Rotations for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST().projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotations: SecretRotationV2Schema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const secretRotations = await server.services.secretRotationV2.listSecretRotationsByProjectId(
|
||||
{ projectId },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATIONS,
|
||||
metadata: {
|
||||
rotationIds: secretRotations.map((sync) => sync.id),
|
||||
count: secretRotations.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotations };
|
||||
}
|
||||
});
|
||||
};
|
@ -1,9 +1,10 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import msFn from "ms";
|
||||
|
||||
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -246,7 +247,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary,
|
||||
...(isTemporary && {
|
||||
expiresIn: ms(ms(temporaryRange || ""), { long: true })
|
||||
expiresIn: msFn(ms(temporaryRange || ""), { long: true })
|
||||
}),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
|
@ -2,6 +2,13 @@ import {
|
||||
TCreateProjectTemplateDTO,
|
||||
TUpdateProjectTemplateDTO
|
||||
} from "@app/ee/services/project-template/project-template-types";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
TCreateSecretRotationV2DTO,
|
||||
TDeleteSecretRotationV2DTO,
|
||||
TSecretRotationV2,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
@ -283,7 +290,15 @@ export enum EventType {
|
||||
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
||||
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
||||
|
||||
GET_SECRET_ROTATIONS = "get-secret-rotations",
|
||||
GET_SECRET_ROTATION = "get-secret-rotation",
|
||||
GET_SECRET_ROTATION_CREDENTIALS = "get-secret-rotation-credentials",
|
||||
CREATE_SECRET_ROTATION = "create-secret-rotation",
|
||||
UPDATE_SECRET_ROTATION = "update-secret-rotation",
|
||||
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||
ROTATE_SECRET_ROTATION = "rotate-secret-rotation"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -2285,6 +2300,56 @@ interface RegisterKmipServerEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretRotationsEvent {
|
||||
type: EventType.GET_SECRET_ROTATIONS;
|
||||
metadata: {
|
||||
type?: SecretRotation;
|
||||
count: number;
|
||||
rotationIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretRotationEvent {
|
||||
type: EventType.GET_SECRET_ROTATION;
|
||||
metadata: {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretRotationCredentialsEvent {
|
||||
type: EventType.GET_SECRET_ROTATION_CREDENTIALS;
|
||||
metadata: {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretRotationEvent {
|
||||
type: EventType.CREATE_SECRET_ROTATION;
|
||||
metadata: Omit<TCreateSecretRotationV2DTO, "projectId"> & { rotationId: string };
|
||||
}
|
||||
|
||||
interface UpdateSecretRotationEvent {
|
||||
type: EventType.UPDATE_SECRET_ROTATION;
|
||||
metadata: TUpdateSecretRotationV2DTO;
|
||||
}
|
||||
|
||||
interface DeleteSecretRotationEvent {
|
||||
type: EventType.DELETE_SECRET_ROTATION;
|
||||
metadata: TDeleteSecretRotationV2DTO;
|
||||
}
|
||||
|
||||
interface RotateSecretRotationEvent {
|
||||
type: EventType.ROTATE_SECRET_ROTATION;
|
||||
metadata: Pick<TSecretRotationV2, "parameters" | "type" | "rotationStatus" | "connectionId" | "folderId"> & {
|
||||
rotationId: string;
|
||||
rotationMessage: string | null;
|
||||
jobId?: string;
|
||||
rotatedAt: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -2495,4 +2560,11 @@ export type Event =
|
||||
| KmipOperationLocateEvent
|
||||
| KmipOperationRegisterEvent
|
||||
| CreateSecretRequestEvent
|
||||
| SecretApprovalRequestReview;
|
||||
| SecretApprovalRequestReview
|
||||
| GetSecretRotationsEvent
|
||||
| GetSecretRotationEvent
|
||||
| GetSecretRotationCredentialsEvent
|
||||
| CreateSecretRotationEvent
|
||||
| UpdateSecretRotationEvent
|
||||
| DeleteSecretRotationEvent
|
||||
| RotateSecretRotationEvent;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
@ -11,6 +10,7 @@ import {
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
@ -1,5 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type PasswordRequirements = {
|
||||
length: number;
|
||||
required: {
|
||||
lowercase: number;
|
||||
uppercase: number;
|
||||
digits: number;
|
||||
symbols: number;
|
||||
};
|
||||
allowedSymbols?: string;
|
||||
};
|
||||
|
||||
export enum SqlProviders {
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql2",
|
||||
@ -100,6 +111,28 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
database: z.string().trim(),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
passwordRequirements: z
|
||||
.object({
|
||||
length: z.number().min(1).max(250),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(0),
|
||||
uppercase: z.number().min(0),
|
||||
digits: z.number().min(0),
|
||||
symbols: z.number().min(0)
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 250;
|
||||
}, "Sum of required characters cannot exceed 250"),
|
||||
allowedSymbols: z.string().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length")
|
||||
.optional()
|
||||
.describe("Password generation requirements"),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomInt } from "crypto";
|
||||
import handlebars from "handlebars";
|
||||
import knex from "knex";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
@ -8,16 +8,99 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const generatePassword = (provider: SqlProviders) => {
|
||||
// oracle has limit of 48 password length
|
||||
const size = provider === SqlProviders.Oracle ? 30 : 48;
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
const ORACLE_PASSWORD_REQUIREMENTS = {
|
||||
...DEFAULT_PASSWORD_REQUIREMENTS,
|
||||
length: 30
|
||||
};
|
||||
|
||||
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
|
||||
const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
const finalReqs = requirements || defaultReqs;
|
||||
|
||||
try {
|
||||
const { length, required, allowedSymbols } = finalReqs;
|
||||
|
||||
const chars = {
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
digits: "0123456789",
|
||||
symbols: allowedSymbols || "-_.~!*"
|
||||
};
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (required.lowercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.lowercase)
|
||||
.fill(0)
|
||||
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.uppercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.uppercase)
|
||||
.fill(0)
|
||||
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.digits > 0) {
|
||||
parts.push(
|
||||
...Array(required.digits)
|
||||
.fill(0)
|
||||
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.symbols > 0) {
|
||||
parts.push(
|
||||
...Array(required.symbols)
|
||||
.fill(0)
|
||||
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||
);
|
||||
}
|
||||
|
||||
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||
|
||||
const allowedChars = Object.entries(chars)
|
||||
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||
.map(([, value]) => value)
|
||||
.join("");
|
||||
|
||||
parts.push(
|
||||
...Array(remainingLength)
|
||||
.fill(0)
|
||||
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||
);
|
||||
|
||||
// shuffle the array to mix up the characters
|
||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||
const j = randomInt(i + 1);
|
||||
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`Failed to generate password: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const generateUsername = (provider: SqlProviders) => {
|
||||
@ -115,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
@ -87,9 +87,14 @@ export const groupServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
const isCustomRole = Boolean(customRole);
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to create a more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const group = await groupDAL.transaction(async (tx) => {
|
||||
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
||||
@ -156,9 +161,13 @@ export const groupServiceFactory = ({
|
||||
);
|
||||
|
||||
const isCustomRole = Boolean(customOrgRole);
|
||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasRequiredNewRolePermission)
|
||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update a more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
if (isCustomRole) customRole = customOrgRole;
|
||||
}
|
||||
|
||||
@ -329,9 +338,13 @@ export const groupServiceFactory = ({
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
||||
if (!hasRequiredPrivileges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to add user to more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||
@ -396,9 +409,13 @@ export const groupServiceFactory = ({
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
||||
if (!hasRequiredPrivileges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to delete user from more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import ms from "ms";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
@ -79,9 +79,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
@ -161,9 +165,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
if (data?.slug) {
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
@ -239,9 +247,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||
return {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
import ms from "ms";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
@ -88,9 +88,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
@ -172,9 +176,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
@ -268,9 +276,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to edit more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import crypto, { KeyObject } from "crypto";
|
||||
import ms from "ms";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
|
@ -32,6 +32,10 @@ export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSecretShareAction {
|
||||
ManageSettings = "manage-settings"
|
||||
}
|
||||
|
||||
export enum OrgPermissionGatewayActions {
|
||||
// is there a better word for this. This mean can an identity be a gateway
|
||||
CreateGateways = "create-gateways",
|
||||
@ -59,7 +63,8 @@ export enum OrgPermissionSubjects {
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway"
|
||||
Gateway = "gateway",
|
||||
SecretShare = "secret-share"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
@ -91,7 +96,8 @@ export type OrgPermissionSet =
|
||||
)
|
||||
]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
|
||||
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
|
||||
|
||||
const AppConnectionConditionSchema = z
|
||||
.object({
|
||||
@ -185,6 +191,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.SecretShare).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSecretShareAction).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
|
||||
@ -292,6 +304,8 @@ const buildAdminPermission = () => {
|
||||
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
|
||||
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,109 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TOrganizations } from "@app/db/schemas";
|
||||
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub,
|
||||
ProjectPermissionV2Schema,
|
||||
SecretSubjectFields
|
||||
} from "./project-permission";
|
||||
|
||||
export function throwIfMissingSecretReadValueOrDescribePermission(
|
||||
permission: MongoAbility<ProjectPermissionSet> | PureAbility,
|
||||
action: Extract<
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
|
||||
>,
|
||||
subjectFields?: SecretSubjectFields
|
||||
) {
|
||||
try {
|
||||
if (subjectFields) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
subject(ProjectPermissionSub.Secrets, subjectFields)
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
if (subjectFields) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function hasSecretReadValueOrDescribePermission(
|
||||
permission: MongoAbility<ProjectPermissionSet>,
|
||||
action: Extract<
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
|
||||
>,
|
||||
subjectFields?: SecretSubjectFields
|
||||
) {
|
||||
let canNewPermission = false;
|
||||
let canOldPermission = false;
|
||||
|
||||
if (subjectFields) {
|
||||
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||
canOldPermission = permission.can(
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
subject(ProjectPermissionSub.Secrets, subjectFields)
|
||||
);
|
||||
} else {
|
||||
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
|
||||
canOldPermission = permission.can(
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
}
|
||||
|
||||
return canNewPermission || canOldPermission;
|
||||
}
|
||||
|
||||
const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional();
|
||||
export function checkForInvalidPermissionCombination(permissions: z.infer<typeof OptionalArrayPermissionSchema>) {
|
||||
if (!permissions) return;
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (permission.subject === ProjectPermissionSub.Secrets) {
|
||||
if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) {
|
||||
const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue);
|
||||
const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret);
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!hasReadValue && !hasDescribeSecret) continue;
|
||||
|
||||
const hasBothDescribeAndReadValue = hasReadValue && hasDescribeSecret;
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `You have selected Read, and ${
|
||||
hasBothDescribeAndReadValue
|
||||
? "both Read Value and Describe Secret"
|
||||
: hasReadValue
|
||||
? "Read Value"
|
||||
: hasDescribeSecret
|
||||
? "Describe Secret"
|
||||
: ""
|
||||
}. You cannot select Read Value or Describe Secret if you have selected Read. The Read permission is a legacy action which has been replaced by Describe Secret and Read Value.`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||
if (!actorAuthMethod) return false;
|
||||
|
||||
|
@ -5,22 +5,6 @@ import { PermissionConditionOperators } from "@app/lib/casl";
|
||||
|
||||
export const PermissionConditionSchema = {
|
||||
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
||||
[PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(),
|
||||
[PermissionConditionOperators.$REGEX]: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(
|
||||
(el) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new RegExp(el);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: "Invalid regex pattern" }
|
||||
),
|
||||
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
||||
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
||||
[PermissionConditionOperators.$GLOB]: z
|
||||
|
@ -17,6 +17,15 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretActions {
|
||||
DescribeAndReadValue = "read",
|
||||
DescribeSecret = "describeSecret",
|
||||
ReadValue = "readValue",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCmekActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@ -44,6 +53,15 @@ export enum ProjectPermissionSecretSyncActions {
|
||||
RemoveSecrets = "remove-secrets"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadCredentials = "read-credentials",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Rotate = "rotate"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionKmipActions {
|
||||
CreateClients = "create-clients",
|
||||
UpdateClients = "update-clients",
|
||||
@ -115,7 +133,7 @@ export type IdentityManagementSubjectFields = {
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||
]
|
||||
| [
|
||||
@ -151,7 +169,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionSecretRotationActions, ProjectPermissionSub.SecretRotation]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||
@ -269,7 +287,7 @@ const GeneralPermissionSchema = [
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretRotationActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
@ -429,6 +447,7 @@ const GeneralPermissionSchema = [
|
||||
})
|
||||
];
|
||||
|
||||
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
|
||||
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||
@ -460,7 +479,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: SecretConditionV2Schema.describe(
|
||||
@ -517,11 +536,9 @@ const buildAdminPermissionRules = () => {
|
||||
|
||||
// Admins get full access to everything
|
||||
[
|
||||
ProjectPermissionSub.Secrets,
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.SecretImports,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
ProjectPermissionSub.Member,
|
||||
ProjectPermissionSub.Groups,
|
||||
ProjectPermissionSub.Role,
|
||||
@ -550,10 +567,22 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
el as ProjectPermissionSub
|
||||
el
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
@ -603,6 +632,18 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
ProjectPermissionSecretRotationActions.Edit,
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSecretRotationActions.ReadCredentials,
|
||||
ProjectPermissionSecretRotationActions.Rotate
|
||||
],
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@ -613,10 +654,12 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
@ -650,7 +693,7 @@ const buildMemberPermissionRules = () => {
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
@ -788,13 +831,15 @@ export const projectMemberPermissions = buildMemberPermissionRules();
|
||||
const buildViewerPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
@ -837,7 +882,6 @@ export const buildServiceTokenProjectPermission = (
|
||||
(subject) => {
|
||||
if (canWrite) {
|
||||
can(ProjectPermissionActions.Edit, subject, {
|
||||
// TODO: @Akhi
|
||||
// @ts-expect-error type
|
||||
secretPath: { $glob: secretPath },
|
||||
environment
|
||||
@ -916,7 +960,17 @@ export const backfillPermissionV1SchemaToV2Schema = (
|
||||
subject: ProjectPermissionSub.SecretImports as const
|
||||
}));
|
||||
|
||||
const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({
|
||||
subject: ProjectPermissionSub.Secrets as const,
|
||||
...el,
|
||||
action:
|
||||
el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue)
|
||||
? el.action.concat(ProjectPermissionSecretActions.ReadValue)
|
||||
: el.action
|
||||
}));
|
||||
|
||||
const secretFolderPolicies = secretSubjects
|
||||
|
||||
.map(({ subject, ...el }) => ({
|
||||
...el,
|
||||
// read permission is not needed anymore
|
||||
@ -958,6 +1012,7 @@ export const backfillPermissionV1SchemaToV2Schema = (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error this is valid ts
|
||||
secretImportPolicies,
|
||||
secretPolicies,
|
||||
dynamicSecretPolicies,
|
||||
hasReadOnlyFolder.length ? [] : secretFolderPolicies
|
||||
);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
import ms from "ms";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
@ -76,9 +76,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged user",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
@ -163,9 +167,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
if (dto?.slug) {
|
||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||
|
@ -63,7 +63,7 @@ export const samlConfigServiceFactory = ({
|
||||
kmsService
|
||||
}: TSamlConfigServiceFactoryDep) => {
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
idpCert,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
@ -93,9 +93,9 @@ export const samlConfigServiceFactory = ({
|
||||
orgId,
|
||||
authProvider,
|
||||
isActive,
|
||||
encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob,
|
||||
encryptedSamlCertificate: encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob,
|
||||
encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob,
|
||||
encryptedSamlCertificate: encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob
|
||||
encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob
|
||||
});
|
||||
|
||||
return samlConfig;
|
||||
@ -106,7 +106,7 @@ export const samlConfigServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
cert,
|
||||
idpCert,
|
||||
actorId,
|
||||
issuer,
|
||||
isActive,
|
||||
@ -136,8 +136,8 @@ export const samlConfigServiceFactory = ({
|
||||
updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob;
|
||||
}
|
||||
|
||||
if (cert !== undefined) {
|
||||
updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob;
|
||||
if (idpCert !== undefined) {
|
||||
updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob;
|
||||
}
|
||||
|
||||
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
||||
|
@ -15,7 +15,7 @@ export type TCreateSamlCfgDTO = {
|
||||
isActive: boolean;
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
cert: string;
|
||||
idpCert: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TUpdateSamlCfgDTO = Partial<{
|
||||
@ -23,7 +23,7 @@ export type TUpdateSamlCfgDTO = Partial<{
|
||||
isActive: boolean;
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
cert: string;
|
||||
idpCert: string;
|
||||
}> &
|
||||
TOrgPermission;
|
||||
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
SecretType,
|
||||
TableName,
|
||||
TSecretApprovalRequestsSecretsInsert,
|
||||
TSecretApprovalRequestsSecretsV2Insert
|
||||
} from "@app/db/schemas";
|
||||
@ -57,8 +58,9 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
||||
@ -88,7 +90,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
secretDAL: TSecretDALFactory;
|
||||
secretTagDAL: Pick<
|
||||
TSecretTagDALFactory,
|
||||
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
|
||||
| "findManyTagsById"
|
||||
| "saveTagsToSecret"
|
||||
| "deleteTagsManySecret"
|
||||
| "saveTagsToSecretV2"
|
||||
| "deleteTagsToSecretV2"
|
||||
| "find"
|
||||
>;
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
@ -106,7 +113,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
TSecretV2BridgeDALFactory,
|
||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
|
||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
@ -503,7 +510,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (!hasMinApproval && !isSoftEnforcement)
|
||||
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
|
||||
let mergeStatus;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
// this cycle if for bridged secrets
|
||||
@ -861,7 +868,6 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
if (isSoftEnforcement) {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||
const approverUsers = await userDAL.find({
|
||||
@ -913,10 +919,11 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath
|
||||
});
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
@ -1001,6 +1008,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
: keyName2BlindIndex[secretName];
|
||||
// add tags
|
||||
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
|
||||
|
||||
return {
|
||||
...latestSecretVersions[secretId],
|
||||
...el,
|
||||
@ -1156,7 +1164,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1327,17 +1336,48 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// deleted secrets
|
||||
const deletedSecrets = data[SecretOperations.Delete];
|
||||
if (deletedSecrets && deletedSecrets.length) {
|
||||
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys(
|
||||
const secretsToDeleteInDB = await secretV2BridgeDAL.find({
|
||||
folderId,
|
||||
deletedSecrets.map((el) => ({
|
||||
key: el.secretKey,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
$complex: {
|
||||
operator: "and",
|
||||
value: [
|
||||
{
|
||||
operator: "or",
|
||||
value: deletedSecrets.map((el) => ({
|
||||
operator: "and",
|
||||
value: [
|
||||
{
|
||||
operator: "eq",
|
||||
field: `${TableName.SecretV2}.key` as "key",
|
||||
value: el.secretKey
|
||||
},
|
||||
{
|
||||
operator: "eq",
|
||||
field: "type",
|
||||
value: SecretType.Shared
|
||||
}
|
||||
]
|
||||
}))
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
if (secretsToDeleteInDB.length !== deletedSecrets.length)
|
||||
throw new NotFoundError({
|
||||
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
secretsToDeleteInDB.forEach((el) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: el.key,
|
||||
secretTags: el.tags?.map((i) => i.slug)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
|
||||
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
|
||||
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
|
||||
@ -1363,9 +1403,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const tagsGroupById = groupBy(tags, (i) => i.id);
|
||||
|
||||
commits.forEach((commit) => {
|
||||
let action = ProjectPermissionActions.Create;
|
||||
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
|
||||
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
|
||||
let action = ProjectPermissionSecretActions.Create;
|
||||
if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit;
|
||||
if (commit.op === SecretOperations.Delete) return; // we do the validation on top
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
action,
|
||||
@ -1456,7 +1496,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||
viewSecretValue: true,
|
||||
hasSecretAccess: () => true
|
||||
});
|
||||
// secrets that gets replicated across imports
|
||||
|
@ -0,0 +1,4 @@
|
||||
export * from "./postgres-credentials-rotation-constants";
|
||||
export * from "./postgres-credentials-rotation-fns";
|
||||
export * from "./postgres-credentials-rotation-schemas";
|
||||
export * from "./postgres-credentials-rotation-types";
|
@ -0,0 +1,9 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "PostgreSQL Credentials",
|
||||
type: SecretRotation.PostgresCredentials,
|
||||
connection: AppConnection.Postgres
|
||||
};
|
@ -0,0 +1,66 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
const PostgresCredentialsRotationParametersSchema = z.object({
|
||||
usernameSecretKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username Secret Key Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.usernameSecretKey),
|
||||
passwordSecretKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username Secret Key Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.passwordSecretKey),
|
||||
issueStatement: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Issue Credentials SQL Statement Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.issueStatement),
|
||||
revokeStatement: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Revoke Credentials SQL Statement Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.revokeStatement)
|
||||
});
|
||||
|
||||
const PostgresCredentialsRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
export const PostgresCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.PostgresCredentials).extend({
|
||||
type: z.literal(SecretRotation.PostgresCredentials),
|
||||
parameters: PostgresCredentialsRotationParametersSchema
|
||||
// generatedCredentials: PostgresCredentialsRotationGeneratedCredentialsSchema
|
||||
});
|
||||
|
||||
export const CreatePostgresCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.PostgresCredentials
|
||||
).extend({
|
||||
parameters: PostgresCredentialsRotationParametersSchema
|
||||
});
|
||||
|
||||
export const UpdatePostgresCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.PostgresCredentials
|
||||
).extend({
|
||||
parameters: PostgresCredentialsRotationParametersSchema.optional()
|
||||
});
|
||||
|
||||
export const PostgresCredentialsRotationListItemSchema = z.object({
|
||||
name: z.literal("PostgreSQL Credentials"),
|
||||
connection: z.literal(AppConnection.Postgres),
|
||||
type: z.literal(SecretRotation.PostgresCredentials)
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TPostgresConnection } from "@app/services/app-connection/postgres";
|
||||
|
||||
import {
|
||||
CreatePostgresCredentialsRotationSchema,
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
PostgresCredentialsRotationSchema
|
||||
} from "./postgres-credentials-rotation-schemas";
|
||||
|
||||
export type TPostgresCredentialsRotation = z.infer<typeof PostgresCredentialsRotationSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationInput = z.infer<typeof CreatePostgresCredentialsRotationSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationListItem = z.infer<typeof PostgresCredentialsRotationListItemSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationWithConnection = TPostgresCredentialsRotation & {
|
||||
connection: TPostgresConnection;
|
||||
};
|
@ -0,0 +1,206 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TSecretRotationsV2 } from "@app/db/schemas/secret-rotations-v2";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
export type TSecretRotationV2DALFactory = ReturnType<typeof secretRotationV2DALFactory>;
|
||||
|
||||
type TSecretRotationFindFilter = Parameters<typeof buildFindFilter<TSecretRotationsV2>>[0];
|
||||
|
||||
const baseSecretRotationV2Query = ({
|
||||
filter,
|
||||
db,
|
||||
tx
|
||||
}: {
|
||||
db: TDbClient;
|
||||
filter?: TSecretRotationFindFilter;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
const query = (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(TableName.AppConnection, `${TableName.SecretRotationV2}.connectionId`, `${TableName.AppConnection}.id`)
|
||||
.select(selectAllTableCols(TableName.SecretRotationV2))
|
||||
.select(
|
||||
// environment
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
// entire connection
|
||||
db.ref("name").withSchema(TableName.AppConnection).as("connectionName"),
|
||||
db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"),
|
||||
db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"),
|
||||
db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"),
|
||||
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db.ref("isPlatformManaged").withSchema(TableName.AppConnection).as("connectionIsPlatformManaged")
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filter)));
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const expandSecretRotation = (
|
||||
secretRotation: Awaited<ReturnType<typeof baseSecretRotationV2Query>>[number],
|
||||
folder?: Awaited<ReturnType<TSecretFolderDALFactory["findSecretPathByFolderIds"]>>[number]
|
||||
) => {
|
||||
const {
|
||||
envId,
|
||||
envName,
|
||||
envSlug,
|
||||
connectionApp,
|
||||
connectionName,
|
||||
connectionId,
|
||||
connectionOrgId,
|
||||
connectionEncryptedCredentials,
|
||||
connectionMethod,
|
||||
connectionDescription,
|
||||
connectionCreatedAt,
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionIsPlatformManaged,
|
||||
...el
|
||||
} = secretRotation;
|
||||
|
||||
return {
|
||||
...el,
|
||||
connectionId,
|
||||
environment: envId ? { id: envId, name: envName, slug: envSlug } : null,
|
||||
connection: {
|
||||
app: connectionApp,
|
||||
id: connectionId,
|
||||
name: connectionName,
|
||||
orgId: connectionOrgId,
|
||||
encryptedCredentials: connectionEncryptedCredentials,
|
||||
method: connectionMethod,
|
||||
description: connectionDescription,
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
isPlatformManaged: connectionIsPlatformManaged
|
||||
},
|
||||
folder: folder
|
||||
? {
|
||||
id: folder.id,
|
||||
path: folder.path
|
||||
}
|
||||
: null
|
||||
};
|
||||
};
|
||||
|
||||
export const secretRotationV2DALFactory = (
|
||||
db: TDbClient,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">
|
||||
) => {
|
||||
const secretRotationV2Orm = ormify(db, TableName.SecretRotationV2);
|
||||
|
||||
const find = async (
|
||||
filter: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string },
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const secretRotations = await baseSecretRotationV2Query({ filter, db, tx });
|
||||
|
||||
if (!secretRotations.length) return [];
|
||||
|
||||
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
|
||||
filter.projectId,
|
||||
secretRotations.filter((rotation) => Boolean(rotation.folderId)).map((rotation) => rotation.folderId!)
|
||||
);
|
||||
|
||||
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
|
||||
|
||||
foldersWithPath.forEach((folder) => {
|
||||
if (folder) folderRecord[folder.id] = folder;
|
||||
});
|
||||
|
||||
return secretRotations.map((rotation) =>
|
||||
expandSecretRotation(rotation, rotation.folderId ? folderRecord[rotation.folderId] : undefined)
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const secretRotation = await baseSecretRotationV2Query({
|
||||
filter: { id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
|
||||
if (secretRotation) {
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: Parameters<(typeof secretRotationV2Orm)["create"]>[0]) => {
|
||||
const secretRotation = (await secretRotationV2Orm.transaction(async (tx) => {
|
||||
const rotation = await secretRotationV2Orm.create(data, tx);
|
||||
|
||||
return baseSecretRotationV2Query({
|
||||
filter: { id: rotation.id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
}))!;
|
||||
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
};
|
||||
|
||||
const updateById = async (rotationId: string, data: Parameters<(typeof secretRotationV2Orm)["updateById"]>[1]) => {
|
||||
const secretRotation = (await secretRotationV2Orm.transaction(async (tx) => {
|
||||
const rotation = await secretRotationV2Orm.updateById(rotationId, data, tx);
|
||||
|
||||
return baseSecretRotationV2Query({
|
||||
filter: { id: rotation.id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
}))!;
|
||||
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
};
|
||||
|
||||
const findOne = async (filter: Parameters<(typeof secretRotationV2Orm)["findOne"]>[0], tx?: Knex) => {
|
||||
try {
|
||||
const secretRotation = await baseSecretRotationV2Query({ filter, db, tx }).first();
|
||||
|
||||
if (secretRotation) {
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretRotationV2Orm, find, create, findById, updateById, findOne };
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-login-credentials"
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
// import { AxiosError } from "axios";
|
||||
//
|
||||
// import {
|
||||
// AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
// AwsParameterStoreSyncFns
|
||||
// } from "@app/services/secret-sync/aws-parameter-store";
|
||||
// import {
|
||||
// AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||
// AwsSecretsManagerSyncFns
|
||||
// } from "@app/services/secret-sync/aws-secrets-manager";
|
||||
// import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks";
|
||||
// import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github";
|
||||
// import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
// import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
// import {
|
||||
// TSecretMap,
|
||||
// TSecretSyncListItem,
|
||||
// TSecretSyncWithCredentials
|
||||
// } from "@app/services/secret-sync/secret-sync-types";
|
||||
//
|
||||
// import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
// import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
// import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
|
||||
// import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
||||
// import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
// import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
// import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
// import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
//
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "./secret-rotation-v2-types";
|
||||
|
||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MsSqlCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION // TODO: replace
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
return Object.values(SECRET_ROTATION_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
//
|
||||
// type TSyncSecretDeps = {
|
||||
// appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
// kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
// };
|
||||
//
|
||||
// // const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
// // let secretMap = { ...unprocessedSecretMap };
|
||||
// //
|
||||
// // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
// //
|
||||
// // if (appendSuffix || prependPrefix) {
|
||||
// // secretMap = {};
|
||||
// // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
// // secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
|
||||
// // });
|
||||
// // }
|
||||
// //
|
||||
// // return secretMap;
|
||||
// // };
|
||||
// //
|
||||
// // const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
// // let secretMap = { ...unprocessedSecretMap };
|
||||
// //
|
||||
// // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
// //
|
||||
// // if (appendSuffix || prependPrefix) {
|
||||
// // secretMap = {};
|
||||
// // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
// // let processedKey = key;
|
||||
// //
|
||||
// // if (prependPrefix && processedKey.startsWith(prependPrefix)) {
|
||||
// // processedKey = processedKey.slice(prependPrefix.length);
|
||||
// // }
|
||||
// //
|
||||
// // if (appendSuffix && processedKey.endsWith(appendSuffix)) {
|
||||
// // processedKey = processedKey.slice(0, -appendSuffix.length);
|
||||
// // }
|
||||
// //
|
||||
// // secretMap[processedKey] = value;
|
||||
// // });
|
||||
// // }
|
||||
// //
|
||||
// // return secretMap;
|
||||
// // };
|
||||
//
|
||||
// export const SecretRotationV2Fns = {
|
||||
// syncSecrets: (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// secretMap: TSecretMap,
|
||||
// { kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
// ): Promise<void> => {
|
||||
// // const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
//
|
||||
// switch (secretSync.destination) {
|
||||
// case SecretSync.AWSParameterStore:
|
||||
// return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AWSSecretsManager:
|
||||
// return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GitHub:
|
||||
// return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GCPSecretManager:
|
||||
// return GcpSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureKeyVault:
|
||||
// return azureKeyVaultSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureAppConfiguration:
|
||||
// return azureAppConfigurationSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Databricks:
|
||||
// return databricksSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Humanitec:
|
||||
// return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// default:
|
||||
// throw new Error(
|
||||
// `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// getSecrets: async (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// { kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
// ): Promise<TSecretMap> => {
|
||||
// let secretMap: TSecretMap;
|
||||
// switch (secretSync.destination) {
|
||||
// case SecretSync.AWSParameterStore:
|
||||
// secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.AWSSecretsManager:
|
||||
// secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.GitHub:
|
||||
// secretMap = await GithubSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.GCPSecretManager:
|
||||
// secretMap = await GcpSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.AzureKeyVault:
|
||||
// secretMap = await azureKeyVaultSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.AzureAppConfiguration:
|
||||
// secretMap = await azureAppConfigurationSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.Databricks:
|
||||
// return databricksSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).getSecrets(secretSync);
|
||||
// case SecretSync.Humanitec:
|
||||
// secretMap = await HumanitecSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// default:
|
||||
// throw new Error(
|
||||
// `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return secretMap;
|
||||
// // return stripAffixes(secretSync, secretMap);
|
||||
// },
|
||||
// removeSecrets: (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// secretMap: TSecretMap,
|
||||
// { kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
// ): Promise<void> => {
|
||||
// // const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
//
|
||||
// switch (secretSync.destination) {
|
||||
// case SecretSync.AWSParameterStore:
|
||||
// return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AWSSecretsManager:
|
||||
// return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GitHub:
|
||||
// return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GCPSecretManager:
|
||||
// return GcpSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureKeyVault:
|
||||
// return azureKeyVaultSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureAppConfiguration:
|
||||
// return azureAppConfigurationSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Databricks:
|
||||
// return databricksSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Humanitec:
|
||||
// return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// default:
|
||||
// throw new Error(
|
||||
// `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// const MAX_MESSAGE_LENGTH = 1024;
|
||||
//
|
||||
// export const parseSyncErrorMessage = (err: unknown): string => {
|
||||
// let errorMessage: string;
|
||||
//
|
||||
// if (err instanceof SecretSyncError) {
|
||||
// errorMessage = JSON.stringify({
|
||||
// secretKey: err.secretKey,
|
||||
// error: err.message || parseSyncErrorMessage(err.error)
|
||||
// });
|
||||
// } else if (err instanceof AxiosError) {
|
||||
// errorMessage = err?.response?.data
|
||||
// ? JSON.stringify(err?.response?.data)
|
||||
// : err?.message ?? "An unknown error occurred.";
|
||||
// } else {
|
||||
// errorMessage = (err as Error)?.message || "An unknown error occurred.";
|
||||
// }
|
||||
//
|
||||
// return errorMessage.length <= MAX_MESSAGE_LENGTH
|
||||
// ? errorMessage
|
||||
// : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
|
||||
// };
|
@ -0,0 +1,12 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
|
||||
};
|
@ -0,0 +1,972 @@
|
||||
// import opentelemetry from "@opentelemetry/api";
|
||||
// import { AxiosError } from "axios";
|
||||
// import { Job } from "bullmq";
|
||||
//
|
||||
// import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
||||
// import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
// import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
// import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
// import { getConfig } from "@app/lib/config/env";
|
||||
// import { logger } from "@app/lib/logger";
|
||||
// import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
// import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
// import { ActorType } from "@app/services/auth/auth-type";
|
||||
// import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
// import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
// import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
// import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
// import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
// import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||
// import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
// import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
|
||||
// import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||
// import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||
// import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||
// import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
// import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
|
||||
// import { fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns";
|
||||
// import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
|
||||
// import {
|
||||
// SecretSync,
|
||||
// SecretSyncImportBehavior,
|
||||
// SecretSyncInitialSyncBehavior
|
||||
// } from "@app/services/secret-sync/secret-sync-enums";
|
||||
// import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
// import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns";
|
||||
// import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
// import {
|
||||
// SecretSyncAction,
|
||||
// SecretSyncStatus,
|
||||
// TQueueSecretSyncImportSecretsByIdDTO,
|
||||
// TQueueSecretSyncRemoveSecretsByIdDTO,
|
||||
// TQueueSecretSyncsByPathDTO,
|
||||
// TQueueSecretSyncSyncSecretsByIdDTO,
|
||||
// TQueueSendSecretSyncActionFailedNotificationsDTO,
|
||||
// TSecretMap,
|
||||
// TSecretSyncImportSecretsDTO,
|
||||
// TSecretSyncRaw,
|
||||
// TSecretSyncRemoveSecretsDTO,
|
||||
// TSecretSyncSyncSecretsDTO,
|
||||
// TSecretSyncWithCredentials,
|
||||
// TSendSecretSyncFailedNotificationsJobDTO
|
||||
// } from "@app/services/secret-sync/secret-sync-types";
|
||||
// import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
// import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||
// import { expandSecretReferencesFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||
// import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
// import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
// import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
//
|
||||
// import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
//
|
||||
// export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
//
|
||||
// type TSecretSyncQueueFactoryDep = {
|
||||
// queueService: Pick<TQueueServiceFactory, "queue" | "start">;
|
||||
// kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
// appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
// keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
// folderDAL: TSecretFolderDALFactory;
|
||||
// secretV2BridgeDAL: Pick<
|
||||
// TSecretV2BridgeDALFactory,
|
||||
// | "findByFolderId"
|
||||
// | "find"
|
||||
// | "insertMany"
|
||||
// | "upsertSecretReferences"
|
||||
// | "findBySecretKeys"
|
||||
// | "bulkUpdate"
|
||||
// | "deleteMany"
|
||||
// >;
|
||||
// secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
// secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">;
|
||||
// auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
// projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
// projectDAL: TProjectDALFactory;
|
||||
// smtpService: Pick<TSmtpService, "sendMail">;
|
||||
// projectBotDAL: TProjectBotDALFactory;
|
||||
// secretDAL: TSecretDALFactory;
|
||||
// secretVersionDAL: TSecretVersionDALFactory;
|
||||
// secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
// secretTagDAL: TSecretTagDALFactory;
|
||||
// secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||
// secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
// secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
// resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
// };
|
||||
//
|
||||
// type SecretSyncActionJob = Job<
|
||||
// TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO
|
||||
// >;
|
||||
//
|
||||
// const getRequeueDelay = (failureCount?: number) => {
|
||||
// if (!failureCount) return 0;
|
||||
//
|
||||
// const baseDelay = 1000;
|
||||
// const maxDelay = 30000;
|
||||
//
|
||||
// const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
|
||||
//
|
||||
// const jitter = delay * (0.5 + Math.random() * 0.5);
|
||||
//
|
||||
// return jitter;
|
||||
// };
|
||||
//
|
||||
// export const secretSyncQueueFactory = ({
|
||||
// queueService,
|
||||
// kmsService,
|
||||
// appConnectionDAL,
|
||||
// keyStore,
|
||||
// folderDAL,
|
||||
// secretV2BridgeDAL,
|
||||
// secretImportDAL,
|
||||
// secretSyncDAL,
|
||||
// auditLogService,
|
||||
// projectMembershipDAL,
|
||||
// projectDAL,
|
||||
// smtpService,
|
||||
// projectBotDAL,
|
||||
// secretDAL,
|
||||
// secretVersionDAL,
|
||||
// secretBlindIndexDAL,
|
||||
// secretTagDAL,
|
||||
// secretVersionTagDAL,
|
||||
// secretVersionV2BridgeDAL,
|
||||
// secretVersionTagV2BridgeDAL,
|
||||
// resourceMetadataDAL
|
||||
// }: TSecretSyncQueueFactoryDep) => {
|
||||
// const appCfg = getConfig();
|
||||
//
|
||||
// const integrationMeter = opentelemetry.metrics.getMeter("SecretSyncs");
|
||||
// const syncSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_sync_secrets_errors", {
|
||||
// description: "Secret Sync - sync secrets errors",
|
||||
// unit: "1"
|
||||
// });
|
||||
// const importSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_import_secrets_errors", {
|
||||
// description: "Secret Sync - import secrets errors",
|
||||
// unit: "1"
|
||||
// });
|
||||
// const removeSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_remove_secrets_errors", {
|
||||
// description: "Secret Sync - remove secrets errors",
|
||||
// unit: "1"
|
||||
// });
|
||||
//
|
||||
// const $createManySecretsRawFn = createManySecretsRawFnFactory({
|
||||
// projectDAL,
|
||||
// projectBotDAL,
|
||||
// secretDAL,
|
||||
// secretVersionDAL,
|
||||
// secretBlindIndexDAL,
|
||||
// secretTagDAL,
|
||||
// secretVersionTagDAL,
|
||||
// folderDAL,
|
||||
// kmsService,
|
||||
// secretVersionV2BridgeDAL,
|
||||
// secretV2BridgeDAL,
|
||||
// secretVersionTagV2BridgeDAL,
|
||||
// resourceMetadataDAL
|
||||
// });
|
||||
//
|
||||
// const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
||||
// projectDAL,
|
||||
// projectBotDAL,
|
||||
// secretDAL,
|
||||
// secretVersionDAL,
|
||||
// secretBlindIndexDAL,
|
||||
// secretTagDAL,
|
||||
// secretVersionTagDAL,
|
||||
// folderDAL,
|
||||
// kmsService,
|
||||
// secretVersionV2BridgeDAL,
|
||||
// secretV2BridgeDAL,
|
||||
// secretVersionTagV2BridgeDAL,
|
||||
// resourceMetadataDAL
|
||||
// });
|
||||
//
|
||||
// const $getInfisicalSecrets = async (
|
||||
// secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
|
||||
// includeImports = true
|
||||
// ) => {
|
||||
// const { projectId, folderId, environment, folder } = secretSync;
|
||||
//
|
||||
// if (!folderId || !environment || !folder)
|
||||
// throw new SecretSyncError({
|
||||
// message:
|
||||
// "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path.",
|
||||
// shouldRetry: false
|
||||
// });
|
||||
//
|
||||
// const secretMap: TSecretMap = {};
|
||||
//
|
||||
// const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
// type: KmsDataKey.SecretManager,
|
||||
// projectId
|
||||
// });
|
||||
//
|
||||
// const decryptSecretValue = (value?: Buffer | undefined | null) =>
|
||||
// value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "";
|
||||
//
|
||||
// const { expandSecretReferences } = expandSecretReferencesFactory({
|
||||
// decryptSecretValue,
|
||||
// secretDAL: secretV2BridgeDAL,
|
||||
// folderDAL,
|
||||
// projectId,
|
||||
// canExpandValue: () => true
|
||||
// });
|
||||
//
|
||||
// const secrets = await secretV2BridgeDAL.findByFolderId(folderId);
|
||||
//
|
||||
// await Promise.allSettled(
|
||||
// secrets.map(async (secret) => {
|
||||
// const secretKey = secret.key;
|
||||
// const secretValue = decryptSecretValue(secret.encryptedValue);
|
||||
// const expandedSecretValue = await expandSecretReferences({
|
||||
// environment: environment.slug,
|
||||
// secretPath: folder.path,
|
||||
// skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||
// value: secretValue
|
||||
// });
|
||||
// secretMap[secretKey] = { value: expandedSecretValue || "" };
|
||||
//
|
||||
// if (secret.encryptedComment) {
|
||||
// const commentValue = decryptSecretValue(secret.encryptedComment);
|
||||
// secretMap[secretKey].comment = commentValue;
|
||||
// }
|
||||
//
|
||||
// secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||
// secretMap[secretKey].secretMetadata = secret.secretMetadata;
|
||||
// })
|
||||
// );
|
||||
//
|
||||
// if (!includeImports) return secretMap;
|
||||
//
|
||||
// const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
|
||||
//
|
||||
// if (secretImports.length) {
|
||||
// const importedSecrets = await fnSecretsV2FromImports({
|
||||
// decryptor: decryptSecretValue,
|
||||
// folderDAL,
|
||||
// secretDAL: secretV2BridgeDAL,
|
||||
// expandSecretReferences,
|
||||
// secretImportDAL,
|
||||
// secretImports,
|
||||
// hasSecretAccess: () => true,
|
||||
// viewSecretValue: true
|
||||
// });
|
||||
//
|
||||
// for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
// for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
// const importedSecret = importedSecrets[i].secrets[j];
|
||||
// if (!secretMap[importedSecret.key]) {
|
||||
// secretMap[importedSecret.key] = {
|
||||
// skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||
// comment: importedSecret.secretComment,
|
||||
// value: importedSecret.secretValue || "",
|
||||
// secretMetadata: importedSecret.secretMetadata
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return secretMap;
|
||||
// };
|
||||
//
|
||||
// const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) =>
|
||||
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncSyncSecrets, payload, {
|
||||
// delay: getRequeueDelay(payload.failedToAcquireLockCount), // this is for delaying re-queued jobs if sync is locked
|
||||
// attempts: 5,
|
||||
// backoff: {
|
||||
// type: "exponential",
|
||||
// delay: 3000
|
||||
// },
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true
|
||||
// });
|
||||
//
|
||||
// const queueSecretSyncImportSecretsById = async (payload: TQueueSecretSyncImportSecretsByIdDTO) =>
|
||||
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncImportSecrets, payload, {
|
||||
// attempts: 1,
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true
|
||||
// });
|
||||
//
|
||||
// const queueSecretSyncRemoveSecretsById = async (payload: TQueueSecretSyncRemoveSecretsByIdDTO) =>
|
||||
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncRemoveSecrets, payload, {
|
||||
// attempts: 1,
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true
|
||||
// });
|
||||
//
|
||||
// const $queueSendSecretSyncFailedNotifications = async (payload: TQueueSendSecretSyncActionFailedNotificationsDTO) => {
|
||||
// if (!appCfg.isSmtpConfigured) return;
|
||||
//
|
||||
// await queueService.queue(
|
||||
// QueueName.AppConnectionSecretSync,
|
||||
// QueueJobs.SecretSyncSendActionFailedNotifications,
|
||||
// payload,
|
||||
// {
|
||||
// jobId: `secret-sync-${payload.secretSync.id}-failed-notifications`,
|
||||
// attempts: 5,
|
||||
// delay: 1000 * 60,
|
||||
// backoff: {
|
||||
// type: "exponential",
|
||||
// delay: 3000
|
||||
// },
|
||||
// removeOnFail: true,
|
||||
// removeOnComplete: true
|
||||
// }
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// const $importSecrets = async (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// importBehavior: SecretSyncImportBehavior
|
||||
// ): Promise<TSecretMap> => {
|
||||
// const { projectId, environment, folder } = secretSync;
|
||||
//
|
||||
// if (!environment || !folder)
|
||||
// throw new Error(
|
||||
// "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path."
|
||||
// );
|
||||
//
|
||||
// const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// if (!Object.keys(importedSecrets).length) return {};
|
||||
//
|
||||
// const importedSecretMap: TSecretMap = {};
|
||||
//
|
||||
// const secretMap = await $getInfisicalSecrets(secretSync, false);
|
||||
//
|
||||
// const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = [];
|
||||
// const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = [];
|
||||
//
|
||||
// Object.entries(importedSecrets).forEach(([key, secretData]) => {
|
||||
// const { value, comment = "", skipMultilineEncoding } = secretData;
|
||||
//
|
||||
// const secret = {
|
||||
// secretName: key,
|
||||
// secretValue: value,
|
||||
// type: SecretType.Shared,
|
||||
// secretComment: comment,
|
||||
// skipMultilineEncoding: skipMultilineEncoding ?? undefined
|
||||
// };
|
||||
//
|
||||
// if (Object.hasOwn(secretMap, key)) {
|
||||
// secretsToUpdate.push(secret);
|
||||
// if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
|
||||
// } else {
|
||||
// secretsToCreate.push(secret);
|
||||
// importedSecretMap[key] = secretData;
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (secretsToCreate.length) {
|
||||
// await $createManySecretsRawFn({
|
||||
// projectId,
|
||||
// path: folder.path,
|
||||
// environment: environment.slug,
|
||||
// secrets: secretsToCreate
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination && secretsToUpdate.length) {
|
||||
// await $updateManySecretsRawFn({
|
||||
// projectId,
|
||||
// path: folder.path,
|
||||
// environment: environment.slug,
|
||||
// secrets: secretsToUpdate
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return importedSecretMap;
|
||||
// };
|
||||
//
|
||||
// const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO) => {
|
||||
// const {
|
||||
// data: { syncId, auditLogInfo }
|
||||
// } = job;
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.findById(syncId);
|
||||
//
|
||||
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
//
|
||||
// await secretSyncDAL.updateById(syncId, {
|
||||
// syncStatus: SecretSyncStatus.Running
|
||||
// });
|
||||
//
|
||||
// logger.info(
|
||||
// `SecretSync Sync [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// let isSynced = false;
|
||||
// let syncMessage: string | null = null;
|
||||
// let isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
//
|
||||
// try {
|
||||
// const {
|
||||
// connection: { orgId, encryptedCredentials }
|
||||
// } = secretSync;
|
||||
//
|
||||
// const credentials = await decryptAppConnectionCredentials({
|
||||
// orgId,
|
||||
// encryptedCredentials,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// const secretSyncWithCredentials = {
|
||||
// ...secretSync,
|
||||
// connection: {
|
||||
// ...secretSync.connection,
|
||||
// credentials
|
||||
// }
|
||||
// } as TSecretSyncWithCredentials;
|
||||
//
|
||||
// const {
|
||||
// lastSyncedAt,
|
||||
// syncOptions: { initialSyncBehavior }
|
||||
// } = secretSyncWithCredentials;
|
||||
//
|
||||
// const secretMap = await $getInfisicalSecrets(secretSync);
|
||||
//
|
||||
// if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) {
|
||||
// const importedSecretMap = await $importSecrets(
|
||||
// secretSyncWithCredentials,
|
||||
// initialSyncBehavior === SecretSyncInitialSyncBehavior.ImportPrioritizeSource
|
||||
// ? SecretSyncImportBehavior.PrioritizeSource
|
||||
// : SecretSyncImportBehavior.PrioritizeDestination
|
||||
// );
|
||||
//
|
||||
// Object.entries(importedSecretMap).forEach(([key, secretData]) => {
|
||||
// secretMap[key] = secretData;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// isSynced = true;
|
||||
// } catch (err) {
|
||||
// logger.error(
|
||||
// err,
|
||||
// `SecretSync Sync Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
// syncSecretsErrorHistogram.record(1, {
|
||||
// version: 1,
|
||||
// destination: secretSync.destination,
|
||||
// syncId: secretSync.id,
|
||||
// projectId: secretSync.projectId,
|
||||
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
|
||||
// status: err instanceof AxiosError ? err.response?.status : undefined,
|
||||
// name: err instanceof Error ? err.name : undefined
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// syncMessage = parseSyncErrorMessage(err);
|
||||
//
|
||||
// if (err instanceof SecretSyncError && !err.shouldRetry) {
|
||||
// isFinalAttempt = true;
|
||||
// } else {
|
||||
// // re-throw so job fails
|
||||
// throw err;
|
||||
// }
|
||||
// } finally {
|
||||
// const ranAt = new Date();
|
||||
// const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
|
||||
//
|
||||
// await auditLogService.createAuditLog({
|
||||
// projectId: secretSync.projectId,
|
||||
// ...(auditLogInfo ?? {
|
||||
// actor: {
|
||||
// type: ActorType.PLATFORM,
|
||||
// metadata: {}
|
||||
// }
|
||||
// }),
|
||||
// event: {
|
||||
// type: EventType.SECRET_SYNC_SYNC_SECRETS,
|
||||
// metadata: {
|
||||
// syncId: secretSync.id,
|
||||
// syncOptions: secretSync.syncOptions,
|
||||
// destination: secretSync.destination,
|
||||
// destinationConfig: secretSync.destinationConfig,
|
||||
// folderId: secretSync.folderId,
|
||||
// connectionId: secretSync.connectionId,
|
||||
// jobRanAt: ranAt,
|
||||
// jobId: job.id!,
|
||||
// syncStatus,
|
||||
// syncMessage
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (isSynced || isFinalAttempt) {
|
||||
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||
// syncStatus,
|
||||
// lastSyncJobId: job.id,
|
||||
// lastSyncMessage: syncMessage,
|
||||
// lastSyncedAt: isSynced ? ranAt : undefined
|
||||
// });
|
||||
//
|
||||
// if (!isSynced) {
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync: updatedSecretSync,
|
||||
// action: SecretSyncAction.SyncSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// logger.info("SecretSync Sync Job with ID %s Completed", job.id);
|
||||
// };
|
||||
//
|
||||
// const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => {
|
||||
// const {
|
||||
// data: { syncId, auditLogInfo, importBehavior }
|
||||
// } = job;
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.findById(syncId);
|
||||
//
|
||||
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
//
|
||||
// await secretSyncDAL.updateById(syncId, {
|
||||
// importStatus: SecretSyncStatus.Running
|
||||
// });
|
||||
//
|
||||
// logger.info(
|
||||
// `SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// let isSuccess = false;
|
||||
// let importMessage: string | null = null;
|
||||
// const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
//
|
||||
// try {
|
||||
// const {
|
||||
// connection: { orgId, encryptedCredentials }
|
||||
// } = secretSync;
|
||||
//
|
||||
// const credentials = await decryptAppConnectionCredentials({
|
||||
// orgId,
|
||||
// encryptedCredentials,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// await $importSecrets(
|
||||
// {
|
||||
// ...secretSync,
|
||||
// connection: {
|
||||
// ...secretSync.connection,
|
||||
// credentials
|
||||
// }
|
||||
// } as TSecretSyncWithCredentials,
|
||||
// importBehavior
|
||||
// );
|
||||
//
|
||||
// isSuccess = true;
|
||||
// } catch (err) {
|
||||
// logger.error(
|
||||
// err,
|
||||
// `SecretSync Import Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
// importSecretsErrorHistogram.record(1, {
|
||||
// version: 1,
|
||||
// destination: secretSync.destination,
|
||||
// syncId: secretSync.id,
|
||||
// projectId: secretSync.projectId,
|
||||
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
|
||||
// status: err instanceof AxiosError ? err.response?.status : undefined,
|
||||
// name: err instanceof Error ? err.name : undefined
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// importMessage = parseSyncErrorMessage(err);
|
||||
//
|
||||
// // re-throw so job fails
|
||||
// throw err;
|
||||
// } finally {
|
||||
// const ranAt = new Date();
|
||||
// const importStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
|
||||
//
|
||||
// await auditLogService.createAuditLog({
|
||||
// projectId: secretSync.projectId,
|
||||
// ...(auditLogInfo ?? {
|
||||
// actor: {
|
||||
// type: ActorType.PLATFORM,
|
||||
// metadata: {}
|
||||
// }
|
||||
// }),
|
||||
// event: {
|
||||
// type: EventType.SECRET_SYNC_IMPORT_SECRETS,
|
||||
// metadata: {
|
||||
// syncId: secretSync.id,
|
||||
// syncOptions: secretSync.syncOptions,
|
||||
// destination: secretSync.destination,
|
||||
// destinationConfig: secretSync.destinationConfig,
|
||||
// folderId: secretSync.folderId,
|
||||
// connectionId: secretSync.connectionId,
|
||||
// jobRanAt: ranAt,
|
||||
// jobId: job.id!,
|
||||
// importStatus,
|
||||
// importMessage,
|
||||
// importBehavior
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (isSuccess || isFinalAttempt) {
|
||||
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||
// importStatus,
|
||||
// lastImportJobId: job.id,
|
||||
// lastImportMessage: importMessage,
|
||||
// lastImportedAt: isSuccess ? ranAt : undefined
|
||||
// });
|
||||
//
|
||||
// if (!isSuccess) {
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync: updatedSecretSync,
|
||||
// action: SecretSyncAction.ImportSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// logger.info("SecretSync Import Job with ID %s Completed", job.id);
|
||||
// };
|
||||
//
|
||||
// const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO) => {
|
||||
// const {
|
||||
// data: { syncId, auditLogInfo, deleteSyncOnComplete }
|
||||
// } = job;
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.findById(syncId);
|
||||
//
|
||||
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
//
|
||||
// await secretSyncDAL.updateById(syncId, {
|
||||
// removeStatus: SecretSyncStatus.Running
|
||||
// });
|
||||
//
|
||||
// logger.info(
|
||||
// `SecretSync Remove [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// let isSuccess = false;
|
||||
// let removeMessage: string | null = null;
|
||||
// const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
//
|
||||
// try {
|
||||
// const {
|
||||
// connection: { orgId, encryptedCredentials }
|
||||
// } = secretSync;
|
||||
//
|
||||
// const credentials = await decryptAppConnectionCredentials({
|
||||
// orgId,
|
||||
// encryptedCredentials,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// const secretMap = await $getInfisicalSecrets(secretSync);
|
||||
//
|
||||
// await SecretSyncFns.removeSecrets(
|
||||
// {
|
||||
// ...secretSync,
|
||||
// connection: {
|
||||
// ...secretSync.connection,
|
||||
// credentials
|
||||
// }
|
||||
// } as TSecretSyncWithCredentials,
|
||||
// secretMap,
|
||||
// {
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// isSuccess = true;
|
||||
// } catch (err) {
|
||||
// logger.error(
|
||||
// err,
|
||||
// `SecretSync Remove Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
// removeSecretsErrorHistogram.record(1, {
|
||||
// version: 1,
|
||||
// destination: secretSync.destination,
|
||||
// syncId: secretSync.id,
|
||||
// projectId: secretSync.projectId,
|
||||
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
|
||||
// status: err instanceof AxiosError ? err.response?.status : undefined,
|
||||
// name: err instanceof Error ? err.name : undefined
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// removeMessage = parseSyncErrorMessage(err);
|
||||
//
|
||||
// // re-throw so job fails
|
||||
// throw err;
|
||||
// } finally {
|
||||
// const ranAt = new Date();
|
||||
// const removeStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
|
||||
//
|
||||
// await auditLogService.createAuditLog({
|
||||
// projectId: secretSync.projectId,
|
||||
// ...(auditLogInfo ?? {
|
||||
// actor: {
|
||||
// type: ActorType.PLATFORM,
|
||||
// metadata: {}
|
||||
// }
|
||||
// }),
|
||||
// event: {
|
||||
// type: EventType.SECRET_SYNC_REMOVE_SECRETS,
|
||||
// metadata: {
|
||||
// syncId: secretSync.id,
|
||||
// syncOptions: secretSync.syncOptions,
|
||||
// destination: secretSync.destination,
|
||||
// destinationConfig: secretSync.destinationConfig,
|
||||
// folderId: secretSync.folderId,
|
||||
// connectionId: secretSync.connectionId,
|
||||
// jobRanAt: ranAt,
|
||||
// jobId: job.id!,
|
||||
// removeStatus,
|
||||
// removeMessage
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (isSuccess || isFinalAttempt) {
|
||||
// if (isSuccess && deleteSyncOnComplete) {
|
||||
// await secretSyncDAL.deleteById(secretSync.id);
|
||||
// } else {
|
||||
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||
// removeStatus,
|
||||
// lastRemoveJobId: job.id,
|
||||
// lastRemoveMessage: removeMessage,
|
||||
// lastRemovedAt: isSuccess ? ranAt : undefined
|
||||
// });
|
||||
//
|
||||
// if (!isSuccess) {
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync: updatedSecretSync,
|
||||
// action: SecretSyncAction.RemoveSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// logger.info("SecretSync Remove Job with ID %s Completed", job.id);
|
||||
// };
|
||||
//
|
||||
// const $sendSecretSyncFailedNotifications = async (job: TSendSecretSyncFailedNotificationsJobDTO) => {
|
||||
// const {
|
||||
// data: { secretSync, auditLogInfo, action }
|
||||
// } = job;
|
||||
//
|
||||
// const { projectId, destination, name, folder, lastSyncMessage, lastRemoveMessage, lastImportMessage, environment } =
|
||||
// secretSync;
|
||||
//
|
||||
// const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
// const project = await projectDAL.findById(projectId);
|
||||
//
|
||||
// let projectAdmins = projectMembers.filter((member) =>
|
||||
// member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
||||
// );
|
||||
//
|
||||
// const triggeredByUserId =
|
||||
// auditLogInfo && auditLogInfo.actor.type === ActorType.USER && auditLogInfo.actor.metadata.userId;
|
||||
//
|
||||
// // only notify triggering user if triggered by admin
|
||||
// if (triggeredByUserId && projectAdmins.map((admin) => admin.userId).includes(triggeredByUserId)) {
|
||||
// projectAdmins = projectAdmins.filter((admin) => admin.userId === triggeredByUserId);
|
||||
// }
|
||||
//
|
||||
// const syncDestination = SECRET_SYNC_NAME_MAP[destination as SecretSync];
|
||||
//
|
||||
// let actionLabel: string;
|
||||
// let failureMessage: string | null | undefined;
|
||||
//
|
||||
// switch (action) {
|
||||
// case SecretSyncAction.ImportSecrets:
|
||||
// actionLabel = "Import";
|
||||
// failureMessage = lastImportMessage;
|
||||
//
|
||||
// break;
|
||||
// case SecretSyncAction.RemoveSecrets:
|
||||
// actionLabel = "Remove";
|
||||
// failureMessage = lastRemoveMessage;
|
||||
//
|
||||
// break;
|
||||
// case SecretSyncAction.SyncSecrets:
|
||||
// default:
|
||||
// actionLabel = `Sync`;
|
||||
// failureMessage = lastSyncMessage;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// await smtpService.sendMail({
|
||||
// recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
// template: SmtpTemplates.SecretSyncFailed,
|
||||
// subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
// substitutions: {
|
||||
// syncName: name,
|
||||
// syncDestination,
|
||||
// content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`,
|
||||
// failureMessage,
|
||||
// secretPath: folder?.path,
|
||||
// environment: environment?.name,
|
||||
// projectName: project.name,
|
||||
// syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// const queueSecretSyncsSyncSecretsByPath = async ({
|
||||
// secretPath,
|
||||
// projectId,
|
||||
// environmentSlug
|
||||
// }: TQueueSecretSyncsByPathDTO) => {
|
||||
// const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
|
||||
//
|
||||
// if (!folder)
|
||||
// throw new Error(
|
||||
// `Could not find folder at path "${secretPath}" for environment with slug "${environmentSlug}" in project with ID "${projectId}"`
|
||||
// );
|
||||
//
|
||||
// const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true });
|
||||
//
|
||||
// await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id })));
|
||||
// };
|
||||
//
|
||||
// const $handleAcquireLockFailure = async (job: SecretSyncActionJob) => {
|
||||
// const { syncId, auditLogInfo } = job.data;
|
||||
//
|
||||
// switch (job.name) {
|
||||
// case QueueJobs.SecretSyncSyncSecrets: {
|
||||
// const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
|
||||
//
|
||||
// if (failedToAcquireLockCount < 10) {
|
||||
// await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.updateById(syncId, {
|
||||
// syncStatus: SecretSyncStatus.Failed,
|
||||
// lastSyncMessage:
|
||||
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
|
||||
// lastSyncJobId: job.id
|
||||
// });
|
||||
//
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync,
|
||||
// action: SecretSyncAction.SyncSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
// // Scott: the two cases below are unlikely to happen as we check the lock at the API level but including this as a fallback
|
||||
// case QueueJobs.SecretSyncImportSecrets: {
|
||||
// const secretSync = await secretSyncDAL.updateById(syncId, {
|
||||
// importStatus: SecretSyncStatus.Failed,
|
||||
// lastImportMessage:
|
||||
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
|
||||
// lastImportJobId: job.id
|
||||
// });
|
||||
//
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync,
|
||||
// action: SecretSyncAction.ImportSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
// case QueueJobs.SecretSyncRemoveSecrets: {
|
||||
// const secretSync = await secretSyncDAL.updateById(syncId, {
|
||||
// removeStatus: SecretSyncStatus.Failed,
|
||||
// lastRemoveMessage:
|
||||
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
|
||||
// lastRemoveJobId: job.id
|
||||
// });
|
||||
//
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync,
|
||||
// action: SecretSyncAction.RemoveSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
// default:
|
||||
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
// throw new Error(`Unhandled Secret Sync Job ${job.name}`);
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// queueService.start(QueueName.AppConnectionSecretSync, async (job) => {
|
||||
// if (job.name === QueueJobs.SecretSyncSendActionFailedNotifications) {
|
||||
// await $sendSecretSyncFailedNotifications(job as TSendSecretSyncFailedNotificationsJobDTO);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const { syncId } = job.data as
|
||||
// | TQueueSecretSyncSyncSecretsByIdDTO
|
||||
// | TQueueSecretSyncImportSecretsByIdDTO
|
||||
// | TQueueSecretSyncRemoveSecretsByIdDTO;
|
||||
//
|
||||
// let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
|
||||
//
|
||||
// try {
|
||||
// lock = await keyStore.acquireLock(
|
||||
// [KeyStorePrefixes.SecretSyncLock(syncId)],
|
||||
// // scott: not sure on this duration; syncs can take excessive amounts of time so we need to keep it locked,
|
||||
// // but should always release below...
|
||||
// 5 * 60 * 1000
|
||||
// );
|
||||
// } catch (e) {
|
||||
// logger.info(`SecretSync Failed to acquire lock [syncId=${syncId}] [job=${job.name}]`);
|
||||
//
|
||||
// await $handleAcquireLockFailure(job as SecretSyncActionJob);
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// switch (job.name) {
|
||||
// case QueueJobs.SecretSyncSyncSecrets:
|
||||
// await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO);
|
||||
// break;
|
||||
// case QueueJobs.SecretSyncImportSecrets:
|
||||
// await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO);
|
||||
// break;
|
||||
// case QueueJobs.SecretSyncRemoveSecrets:
|
||||
// await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO);
|
||||
// break;
|
||||
// default:
|
||||
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
// throw new Error(`Unhandled Secret Sync Job ${job.name}`);
|
||||
// }
|
||||
// } finally {
|
||||
// await lock.release();
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return {
|
||||
// queueSecretSyncSyncSecretsById,
|
||||
// queueSecretSyncImportSecretsById,
|
||||
// queueSecretSyncRemoveSecretsById,
|
||||
// queueSecretSyncsSyncSecretsByPath
|
||||
// };
|
||||
// };
|
@ -0,0 +1,69 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationsV2Schema } from "@app/db/schemas/secret-rotations-v2";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
export const BaseSecretRotationSchema = (type: SecretRotation) =>
|
||||
SecretRotationsV2Schema.omit({
|
||||
type: true,
|
||||
parameters: true,
|
||||
encryptedGeneratedCredentials: true
|
||||
}).extend({
|
||||
connection: z.object({
|
||||
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
||||
name: z.string(),
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }).nullable(),
|
||||
folder: z.object({ id: z.string(), path: z.string() }).nullable()
|
||||
});
|
||||
|
||||
export const BaseCreateSecretRotationSchema = (type: SecretRotation) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretRotations.CREATE(type).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.CREATE(type).projectId),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(SecretRotations.CREATE(type).description),
|
||||
connectionId: z.string().uuid().describe(SecretRotations.CREATE(type).connectionId),
|
||||
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.CREATE(type).environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Secret path required")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(SecretRotations.CREATE(type).secretPath),
|
||||
isAutoRotationEnabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(SecretRotations.CREATE(type).isAutoRotationEnabled),
|
||||
interval: z.coerce.number().describe(SecretRotations.CREATE(type).interval)
|
||||
});
|
||||
|
||||
export const BaseUpdateSecretRotationSchema = (type: SecretRotation) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretRotations.UPDATE(type).name),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(SecretRotations.UPDATE(type).description),
|
||||
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.UPDATE(type).environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Secret path required")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(SecretRotations.UPDATE(type).secretPath),
|
||||
isAutoRotationEnabled: z.boolean().default(true).describe(SecretRotations.UPDATE(type).isAutoRotationEnabled),
|
||||
interval: z.coerce.number().describe(SecretRotations.UPDATE(type).interval)
|
||||
});
|
@ -0,0 +1,548 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { listSecretRotationOptions } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
|
||||
import {
|
||||
SECRET_ROTATION_CONNECTION_MAP,
|
||||
SECRET_ROTATION_NAME_MAP
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import {
|
||||
TCreateSecretRotationV2DTO,
|
||||
TDeleteSecretRotationV2DTO,
|
||||
TFindSecretRotationV2ByIdDTO,
|
||||
TFindSecretRotationV2ByNameDTO,
|
||||
TListSecretRotationsV2ByProjectId,
|
||||
TSecretRotationV2,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
|
||||
type TSecretRotationV2ServiceFactoryDep = {
|
||||
secretRotationV2DAL: TSecretRotationV2DALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
||||
// keyStore: Pick<TKeyStoreFactory, "getItem">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
secretRotationV2DAL,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectBotService,
|
||||
licenseService
|
||||
}: TSecretRotationV2ServiceFactoryDep) => {
|
||||
const listSecretRotationsByProjectId = async (
|
||||
{ projectId, type }: TListSecretRotationsV2ByProjectId,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotations due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationV2DAL.find({
|
||||
...(type && { type }),
|
||||
projectId
|
||||
});
|
||||
|
||||
return secretRotations as TSecretRotationV2[];
|
||||
};
|
||||
|
||||
const findSecretRotationById = async ({ type, rotationId }: TFindSecretRotationV2ByIdDTO, actor: OrgServiceActor) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret Rotation with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
};
|
||||
|
||||
const findSecretRotationByName = async (
|
||||
{ type, rotationName, projectId }: TFindSecretRotationV2ByNameDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
// we prevent conflicting names within a project
|
||||
const secretRotation = await secretRotationV2DAL.findOne({
|
||||
name: rotationName,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with name "${rotationName}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret Rotation with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
};
|
||||
|
||||
const createSecretRotation = async (
|
||||
{ projectId, secretPath, environment, ...params }: TCreateSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create secret rotation due to plan restriction. Upgrade plan to create secret rotations."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
if (!shouldUseSecretV2Bridge)
|
||||
throw new BadRequestError({ message: "Project version does not support Secret Rotation V2" });
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
|
||||
if (!folder)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
const typeApp = SECRET_ROTATION_CONNECTION_MAP[params.type];
|
||||
|
||||
// validates permission to connect and app is valid for sync type
|
||||
await appConnectionService.connectAppConnectionById(typeApp, params.connectionId, actor);
|
||||
|
||||
// TODO: initialize credentials
|
||||
|
||||
try {
|
||||
const secretRotation = await secretRotationV2DAL.create({
|
||||
folderId: folder.id,
|
||||
...params,
|
||||
encryptedGeneratedCredentials: Buffer.from([]),
|
||||
projectId
|
||||
});
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `A Secret Rotation with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecretRotation = async (
|
||||
{ type, rotationId, secretPath, environment, ...params }: TUpdateSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations."
|
||||
});
|
||||
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Edit,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
let { folderId } = secretRotation;
|
||||
|
||||
if (
|
||||
(secretPath && secretPath !== secretRotation.folder?.path) ||
|
||||
(environment && environment !== secretRotation.environment?.slug)
|
||||
) {
|
||||
const updatedEnvironment = environment ?? secretRotation.environment?.slug;
|
||||
const updatedSecretPath = secretPath ?? secretRotation.folder?.path;
|
||||
|
||||
if (!updatedEnvironment || !updatedSecretPath)
|
||||
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
|
||||
|
||||
// TODO: get secrets to determine delete permission
|
||||
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretActions.Create,
|
||||
// subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
// );
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: updatedEnvironment, secretPath: updatedSecretPath })
|
||||
);
|
||||
|
||||
const newFolder = await folderDAL.findBySecretPath(
|
||||
secretRotation.projectId,
|
||||
updatedEnvironment,
|
||||
updatedSecretPath
|
||||
);
|
||||
|
||||
if (!newFolder)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretRotation.projectId}"`
|
||||
});
|
||||
|
||||
folderId = newFolder.id;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedSecretRotation = await secretRotationV2DAL.updateById(rotationId, {
|
||||
...params,
|
||||
folderId
|
||||
});
|
||||
|
||||
return updatedSecretRotation as TSecretRotationV2;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `A Secret Rotation with the name "${params.name}" already exists for the project with ID "${secretRotation.projectId}"`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSecretRotation = async (
|
||||
{ type, rotationId, removeSecrets }: TDeleteSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
if (removeSecrets) {
|
||||
// TODO: get secrets to determine remove permissions
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.RemoveSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
// TODO: remove secrets
|
||||
} else {
|
||||
// TODO delete relations
|
||||
}
|
||||
|
||||
await secretRotationV2DAL.deleteById(rotationId);
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
};
|
||||
//
|
||||
// const triggerSecretRotationRotationSecretsById = async (
|
||||
// { rotationId, type, ...params }: TTriggerSecretRotationRotationSecretsByIdDTO,
|
||||
// actor: OrgServiceActor
|
||||
// ) => {
|
||||
// const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
//
|
||||
// if (!secretRotation)
|
||||
// throw new NotFoundError({
|
||||
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
// });
|
||||
//
|
||||
// const { permission } = await permissionService.getProjectPermission({
|
||||
// actor: actor.type,
|
||||
// actorId: actor.id,
|
||||
// actorAuthMethod: actor.authMethod,
|
||||
// actorOrgId: actor.orgId,
|
||||
// actionProjectType: ActionProjectType.SecretManager,
|
||||
// projectId: secretRotation.projectId
|
||||
// });
|
||||
//
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.RotationSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
//
|
||||
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
// throw new BadRequestError({
|
||||
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
// });
|
||||
//
|
||||
// if (!secretRotation.folderId)
|
||||
// throw new BadRequestError({
|
||||
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
|
||||
// });
|
||||
//
|
||||
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
|
||||
//
|
||||
// if (isRotationJobRunning)
|
||||
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||
//
|
||||
// await secretRotationQueue.queueSecretRotationRotationSecretsById({ rotationId, ...params });
|
||||
//
|
||||
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
|
||||
// syncStatus: SecretRotationStatus.Pending
|
||||
// });
|
||||
//
|
||||
// return updatedSecretRotation as TSecretRotation;
|
||||
// };
|
||||
//
|
||||
// const triggerSecretRotationImportSecretsById = async (
|
||||
// { rotationId, type, ...params }: TTriggerSecretRotationImportSecretsByIdDTO,
|
||||
// actor: OrgServiceActor
|
||||
// ) => {
|
||||
// if (!listSecretRotationOptions().find((option) => option.type === type)?.canImportSecrets) {
|
||||
// throw new BadRequestError({
|
||||
// message: `${SECRET_ROTATION_NAME_MAP[type]} does not support importing secrets.`
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
//
|
||||
// if (!secretRotation)
|
||||
// throw new NotFoundError({
|
||||
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
// });
|
||||
//
|
||||
// const { permission } = await permissionService.getProjectPermission({
|
||||
// actor: actor.type,
|
||||
// actorId: actor.id,
|
||||
// actorAuthMethod: actor.authMethod,
|
||||
// actorOrgId: actor.orgId,
|
||||
// actionProjectType: ActionProjectType.SecretManager,
|
||||
// projectId: secretRotation.projectId
|
||||
// });
|
||||
//
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.ImportSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
//
|
||||
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
// throw new BadRequestError({
|
||||
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
// });
|
||||
//
|
||||
// if (!secretRotation.folderId)
|
||||
// throw new BadRequestError({
|
||||
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
|
||||
// });
|
||||
//
|
||||
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
|
||||
//
|
||||
// if (isRotationJobRunning)
|
||||
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||
//
|
||||
// await secretRotationQueue.queueSecretRotationImportSecretsById({ rotationId, ...params });
|
||||
//
|
||||
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
|
||||
// importStatus: SecretRotationStatus.Pending
|
||||
// });
|
||||
//
|
||||
// return updatedSecretRotation as TSecretRotation;
|
||||
// };
|
||||
//
|
||||
// const triggerSecretRotationRemoveSecretsById = async (
|
||||
// { rotationId, type, ...params }: TTriggerSecretRotationRemoveSecretsByIdDTO,
|
||||
// actor: OrgServiceActor
|
||||
// ) => {
|
||||
// const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
//
|
||||
// if (!secretRotation)
|
||||
// throw new NotFoundError({
|
||||
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
// });
|
||||
//
|
||||
// const { permission } = await permissionService.getProjectPermission({
|
||||
// actor: actor.type,
|
||||
// actorId: actor.id,
|
||||
// actorAuthMethod: actor.authMethod,
|
||||
// actorOrgId: actor.orgId,
|
||||
// actionProjectType: ActionProjectType.SecretManager,
|
||||
// projectId: secretRotation.projectId
|
||||
// });
|
||||
//
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.RemoveSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
//
|
||||
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
// throw new BadRequestError({
|
||||
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
// });
|
||||
//
|
||||
// if (!secretRotation.folderId)
|
||||
// throw new BadRequestError({
|
||||
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
|
||||
// });
|
||||
//
|
||||
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
|
||||
//
|
||||
// if (isRotationJobRunning)
|
||||
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||
//
|
||||
// await secretRotationQueue.queueSecretRotationRemoveSecretsById({ rotationId, ...params });
|
||||
//
|
||||
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
|
||||
// removeStatus: SecretRotationStatus.Pending
|
||||
// });
|
||||
//
|
||||
// return updatedSecretRotation as TSecretRotation;
|
||||
// };
|
||||
|
||||
return {
|
||||
listSecretRotationOptions,
|
||||
listSecretRotationsByProjectId,
|
||||
createSecretRotation,
|
||||
updateSecretRotation,
|
||||
findSecretRotationById,
|
||||
findSecretRotationByName,
|
||||
deleteSecretRotation
|
||||
// triggerSecretRotationRotationSecretsById,
|
||||
// triggerSecretRotationImportSecretsById,
|
||||
// triggerSecretRotationRemoveSecretsById
|
||||
};
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
import {
|
||||
TPostgresCredentialsRotation,
|
||||
TPostgresCredentialsRotationInput,
|
||||
TPostgresCredentialsRotationListItem,
|
||||
TPostgresCredentialsRotationWithConnection
|
||||
} from "./postgres-credentials";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
export type TSecretRotationV2 = TPostgresCredentialsRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection = TPostgresCredentialsRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem;
|
||||
|
||||
export type TListSecretRotationsV2ByProjectId = {
|
||||
projectId: string;
|
||||
type?: SecretRotation;
|
||||
};
|
||||
|
||||
export type TFindSecretRotationV2ByIdDTO = {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TFindSecretRotationV2ByNameDTO = {
|
||||
rotationName: string;
|
||||
projectId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TCreateSecretRotationV2DTO = Pick<
|
||||
TSecretRotationV2,
|
||||
"parameters" | "description" | "interval" | "name" | "connectionId" | "projectId"
|
||||
> & {
|
||||
type: SecretRotation;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateSecretRotationV2DTO = Partial<Omit<TCreateSecretRotationV2DTO, "projectId" | "connectionId">> & {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TDeleteSecretRotationV2DTO = {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
removeSecrets: boolean;
|
||||
};
|
@ -13,6 +13,7 @@ import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@ -332,6 +333,7 @@ export const secretRotationQueueFactory = ({
|
||||
await secretVersionV2BridgeDAL.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
actorType: ActorType.PLATFORM,
|
||||
secretId: id
|
||||
})),
|
||||
tx
|
||||
|
@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
||||
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
|
||||
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
|
||||
@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({
|
||||
});
|
||||
}
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
|
@ -1,16 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
|
||||
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
|
||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
@ -21,8 +23,16 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import {
|
||||
hasSecretReadValueOrDescribePermission,
|
||||
throwIfMissingSecretReadValueOrDescribePermission
|
||||
} from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import {
|
||||
TGetSnapshotDataDTO,
|
||||
TProjectSnapshotCountDTO,
|
||||
@ -96,10 +106,10 @@ export const secretSnapshotServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
environment,
|
||||
secretPath: path
|
||||
});
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) {
|
||||
@ -133,10 +143,10 @@ export const secretSnapshotServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
environment,
|
||||
secretPath: path
|
||||
});
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder)
|
||||
@ -161,6 +171,7 @@ export const secretSnapshotServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
const shouldUseBridge = snapshot.projectVersion === 3;
|
||||
let snapshotDetails;
|
||||
if (shouldUseBridge) {
|
||||
@ -169,68 +180,112 @@ export const secretSnapshotServiceFactory = ({
|
||||
projectId: snapshot.projectId
|
||||
});
|
||||
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
|
||||
|
||||
const fullFolderPath = await getFullFolderPath({
|
||||
folderDAL,
|
||||
folderId: encryptedSnapshotDetails.folderId,
|
||||
envId: encryptedSnapshotDetails.environment.id
|
||||
});
|
||||
|
||||
snapshotDetails = {
|
||||
...encryptedSnapshotDetails,
|
||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
|
||||
...el,
|
||||
secretKey: el.key,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: ""
|
||||
}))
|
||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
|
||||
const canReadValue = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment: encryptedSnapshotDetails.environment.slug,
|
||||
secretPath: fullFolderPath,
|
||||
secretName: el.key,
|
||||
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
|
||||
}
|
||||
);
|
||||
|
||||
let secretValue = "";
|
||||
if (canReadValue) {
|
||||
secretValue = el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "";
|
||||
} else {
|
||||
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
|
||||
}
|
||||
|
||||
return {
|
||||
...el,
|
||||
secretKey: el.key,
|
||||
secretValueHidden: !canReadValue,
|
||||
secretValue,
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: ""
|
||||
};
|
||||
})
|
||||
};
|
||||
} else {
|
||||
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
|
||||
|
||||
const fullFolderPath = await getFullFolderPath({
|
||||
folderDAL,
|
||||
folderId: encryptedSnapshotDetails.folderId,
|
||||
envId: encryptedSnapshotDetails.environment.id
|
||||
});
|
||||
|
||||
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
|
||||
if (!botKey)
|
||||
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
|
||||
snapshotDetails = {
|
||||
...encryptedSnapshotDetails,
|
||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
|
||||
...el,
|
||||
secretKey: decryptSymmetric128BitHexKeyUTF8({
|
||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretKeyCiphertext,
|
||||
iv: el.secretKeyIV,
|
||||
tag: el.secretKeyTag,
|
||||
key: botKey
|
||||
}),
|
||||
secretValue: decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretValueCiphertext,
|
||||
iv: el.secretValueIV,
|
||||
tag: el.secretValueTag,
|
||||
key: botKey
|
||||
}),
|
||||
secretComment:
|
||||
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
|
||||
? decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretCommentCiphertext,
|
||||
iv: el.secretCommentIV,
|
||||
tag: el.secretCommentTag,
|
||||
key: botKey
|
||||
})
|
||||
: ""
|
||||
}))
|
||||
});
|
||||
|
||||
const canReadValue = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment: encryptedSnapshotDetails.environment.slug,
|
||||
secretPath: fullFolderPath,
|
||||
secretName: secretKey,
|
||||
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
|
||||
}
|
||||
);
|
||||
|
||||
let secretValue = "";
|
||||
|
||||
if (canReadValue) {
|
||||
secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretValueCiphertext,
|
||||
iv: el.secretValueIV,
|
||||
tag: el.secretValueTag,
|
||||
key: botKey
|
||||
});
|
||||
} else {
|
||||
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
|
||||
}
|
||||
|
||||
return {
|
||||
...el,
|
||||
secretKey,
|
||||
secretValueHidden: !canReadValue,
|
||||
secretValue,
|
||||
secretComment:
|
||||
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
|
||||
? decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretCommentCiphertext,
|
||||
iv: el.secretCommentIV,
|
||||
tag: el.secretCommentTag,
|
||||
key: botKey
|
||||
})
|
||||
: ""
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const fullFolderPath = await getFullFolderPath({
|
||||
folderDAL,
|
||||
folderId: snapshotDetails.folderId,
|
||||
envId: snapshotDetails.environment.id
|
||||
});
|
||||
|
||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: snapshotDetails.environment.slug,
|
||||
secretPath: fullFolderPath
|
||||
})
|
||||
);
|
||||
|
||||
return snapshotDetails;
|
||||
};
|
||||
|
||||
@ -370,7 +425,21 @@ export const secretSnapshotServiceFactory = ({
|
||||
const secrets = await secretV2BridgeDAL.insertMany(
|
||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||
secretVersions.map(
|
||||
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
|
||||
({
|
||||
latestSecretVersion,
|
||||
version,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
secretId,
|
||||
envId,
|
||||
id,
|
||||
tags,
|
||||
// exclude the bottom fields from the secret - they are for versioning only.
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType,
|
||||
...el
|
||||
}) => ({
|
||||
...el,
|
||||
id: secretId,
|
||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||
@ -401,8 +470,18 @@ export const secretSnapshotServiceFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
||||
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
||||
const actorType = actor || ActorType.PLATFORM;
|
||||
|
||||
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
|
||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id,
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await secretVersionV2TagBridgeDAL.insertMany(
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
|
||||
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { execFile } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import { promises as fs } from "fs";
|
||||
import ms from "ms";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import {
|
||||
|
@ -1,3 +1,8 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
SECRET_ROTATION_CONNECTION_MAP,
|
||||
SECRET_ROTATION_NAME_MAP
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@ -459,7 +464,8 @@ export const PROJECTS = {
|
||||
workspaceId: "The ID of the project to update.",
|
||||
name: "The new name of the project.",
|
||||
projectDescription: "An optional description label for the project.",
|
||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
||||
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||
slug: "An optional slug for the project. (must be unique within the organization)"
|
||||
},
|
||||
GET_KEY: {
|
||||
workspaceId: "The ID of the project to get the key from."
|
||||
@ -666,6 +672,7 @@ export const SECRETS = {
|
||||
secretPath: "The path of the secret to attach tags to.",
|
||||
type: "The type of the secret to attach tags to. (shared/personal)",
|
||||
environment: "The slug of the environment where the secret is located",
|
||||
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||
projectSlug: "The slug of the project where the secret is located.",
|
||||
tagSlugs: "An array of existing tag slugs to attach to the secret."
|
||||
},
|
||||
@ -689,6 +696,7 @@ export const RAW_SECRETS = {
|
||||
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
|
||||
environment: "The slug of the environment to list secrets from.",
|
||||
secretPath: "The secret path to list secrets from.",
|
||||
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||
includeImports: "Weather to include imported secrets or not.",
|
||||
tagSlugs: "The comma separated tag slugs to filter secrets.",
|
||||
metadataFilter:
|
||||
@ -717,6 +725,7 @@ export const RAW_SECRETS = {
|
||||
secretPath: "The path of the secret to get.",
|
||||
version: "The version of the secret to get.",
|
||||
type: "The type of the secret to get.",
|
||||
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||
includeImports: "Weather to include imported secrets or not."
|
||||
},
|
||||
UPDATE: {
|
||||
@ -1645,7 +1654,8 @@ export const AppConnections = {
|
||||
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||
description: `An optional description for the ${appName} Connection.`,
|
||||
credentials: `The credentials used to connect with ${appName}.`,
|
||||
method: `The method used to authenticate with ${appName}.`
|
||||
method: `The method used to authenticate with ${appName}.`,
|
||||
isPlatformManaged: `Whether or not the ${appName} Connection should be managed by Infisical.`
|
||||
};
|
||||
},
|
||||
UPDATE: (app: AppConnection) => {
|
||||
@ -1655,7 +1665,8 @@ export const AppConnections = {
|
||||
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||
description: `The updated description of the ${appName} Connection.`,
|
||||
credentials: `The credentials used to connect with ${appName}.`,
|
||||
method: `The method used to authenticate with ${appName}.`
|
||||
method: `The method used to authenticate with ${appName}.`,
|
||||
isPlatformManaged: `Whether or not the ${appName} Connection should be managed by Infisical.`
|
||||
};
|
||||
},
|
||||
DELETE: (app: AppConnection) => ({
|
||||
@ -1767,6 +1778,76 @@ export const SecretSyncs = {
|
||||
},
|
||||
DATABRICKS: {
|
||||
scope: "The Databricks secret scope that secrets should be synced to."
|
||||
},
|
||||
HUMANITEC: {
|
||||
app: "The ID of the Humanitec app to sync secrets to.",
|
||||
org: "The ID of the Humanitec org to sync secrets to.",
|
||||
env: "The ID of the Humanitec environment to sync secrets to.",
|
||||
scope: "The Humanitec scope that secrets should be synced to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const SecretRotations = {
|
||||
LIST: (type?: SecretRotation) => ({
|
||||
projectId: `The ID of the project to list ${type ? SECRET_ROTATION_NAME_MAP[type] : "Secret"} Rotations from.`
|
||||
}),
|
||||
GET_BY_ID: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`
|
||||
}),
|
||||
GET_CREDENTIALS_BY_ID: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve the credentials for.`
|
||||
}),
|
||||
GET_BY_NAME: (type: SecretRotation) => ({
|
||||
rotationName: `The name of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`,
|
||||
projectId: `The ID of the project the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is associated with.`
|
||||
}),
|
||||
CREATE: (type: SecretRotation) => {
|
||||
const destinationName = SECRET_ROTATION_NAME_MAP[type];
|
||||
return {
|
||||
name: `The name of the ${destinationName} Rotation to create. Must be slug-friendly.`,
|
||||
description: `An optional description for the ${destinationName} Rotation.`,
|
||||
projectId: "The ID of the project to create the rotation in.",
|
||||
environment: `The slug of the project environment to create the rotation in.`,
|
||||
secretPath: `The folder path to of the project to create the rotation in.`,
|
||||
connectionId: `The ID of the ${
|
||||
APP_CONNECTION_NAME_MAP[SECRET_ROTATION_CONNECTION_MAP[type]]
|
||||
} Connection to use for rotation.`,
|
||||
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified interval has elapsed.`,
|
||||
interval: `The interval, in days, to automatically rotate secrets.`
|
||||
};
|
||||
},
|
||||
UPDATE: (type: SecretRotation) => {
|
||||
const typeName = SECRET_ROTATION_NAME_MAP[type];
|
||||
return {
|
||||
rotationId: `The ID of the ${typeName} Rotation to be updated.`,
|
||||
name: `The updated name of the ${typeName} Rotation. Must be slug-friendly.`,
|
||||
environment: `The updated slug of the project environment to move the rotation to.`,
|
||||
secretPath: `The updated folder path to move the rotation to.`,
|
||||
description: `The updated description of the ${typeName} Rotation.`,
|
||||
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified interval has elapsed.`,
|
||||
interval: `The interval, in days, to automatically rotate secrets.`
|
||||
};
|
||||
},
|
||||
DELETE: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to be deleted.`,
|
||||
removeSecrets: `Whether the secrets belonging to this rotation should be deleted.`
|
||||
}),
|
||||
ROTATE: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to rotate credentials for.`
|
||||
}),
|
||||
PARAMETERS: {
|
||||
POSTGRES_CREDENTIALS: {
|
||||
usernameSecretKey: "The secret key that the generated username will be mapped to.",
|
||||
passwordSecretKey: "The secret key that the generated password will be mapped to.",
|
||||
issueStatement: "The SQL statement to generate the credentials on rotation.",
|
||||
revokeStatement: "The SQL statement to revoke expired credentials on rotation."
|
||||
},
|
||||
MSSQL_CREDENTIALS: {
|
||||
usernameSecretKey: "The secret key that the generated username will be mapped to.",
|
||||
passwordSecretKey: "The secret key that the generated password will be mapped to.",
|
||||
issueStatement: "The SQL statement to generate the credentials on rotation.",
|
||||
revokeStatement: "The SQL statement to revoke expired credentials on rotation."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
669
backend/src/lib/casl/boundary.test.ts
Normal file
669
backend/src/lib/casl/boundary.test.ts
Normal file
@ -0,0 +1,669 @@
|
||||
import { createMongoAbility } from "@casl/ability";
|
||||
|
||||
import { PermissionConditionOperators } from ".";
|
||||
import { validatePermissionBoundary } from "./boundary";
|
||||
|
||||
describe("Validate Permission Boundary Function", () => {
|
||||
test.each([
|
||||
{
|
||||
title: "child with equal privilege",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit", "delete", "read"],
|
||||
subject: "secrets"
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit", "delete", "read"],
|
||||
subject: "secrets"
|
||||
}
|
||||
]),
|
||||
expectValid: true,
|
||||
missingPermissions: []
|
||||
},
|
||||
{
|
||||
title: "child with less privilege",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit", "delete", "read"],
|
||||
subject: "secrets"
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit"],
|
||||
subject: "secrets"
|
||||
}
|
||||
]),
|
||||
expectValid: true,
|
||||
missingPermissions: []
|
||||
},
|
||||
{
|
||||
title: "child with more privilege",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets"
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit"],
|
||||
subject: "secrets"
|
||||
}
|
||||
]),
|
||||
expectValid: false,
|
||||
missingPermissions: [{ action: "edit", subject: "secrets" }]
|
||||
},
|
||||
{
|
||||
title: "parent with multiple and child with multiple",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets"
|
||||
},
|
||||
{
|
||||
action: ["create", "edit"],
|
||||
subject: "members"
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "members"
|
||||
},
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets"
|
||||
}
|
||||
]),
|
||||
expectValid: true,
|
||||
missingPermissions: []
|
||||
},
|
||||
{
|
||||
title: "Child with no access",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets"
|
||||
},
|
||||
{
|
||||
action: ["create", "edit"],
|
||||
subject: "members"
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([]),
|
||||
expectValid: true,
|
||||
missingPermissions: []
|
||||
},
|
||||
{
|
||||
title: "Parent and child disjoint set",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit", "delete", "read"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit", "delete", "read"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
}
|
||||
]),
|
||||
expectValid: false,
|
||||
missingPermissions: ["create", "edit", "delete", "read"].map((el) => ({
|
||||
action: el,
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
}))
|
||||
},
|
||||
{
|
||||
title: "Parent with inverted rules",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit", "delete", "read"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "read",
|
||||
subject: "secrets",
|
||||
inverted: true,
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||
}
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: "read",
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||
}
|
||||
}
|
||||
]),
|
||||
expectValid: true,
|
||||
missingPermissions: []
|
||||
},
|
||||
{
|
||||
title: "Parent with inverted rules - child accessing invalid one",
|
||||
parentPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create", "edit", "delete", "read"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "read",
|
||||
subject: "secrets",
|
||||
inverted: true,
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||
}
|
||||
}
|
||||
]),
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: "read",
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||
}
|
||||
}
|
||||
]),
|
||||
expectValid: false,
|
||||
missingPermissions: [
|
||||
{
|
||||
action: "read",
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
])("Check permission: $title", ({ parentPermission, childPermission, expectValid, missingPermissions }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
if (expectValid) {
|
||||
expect(permissionBoundary.isValid).toBeTruthy();
|
||||
} else {
|
||||
expect(permissionBoundary.isValid).toBeFalsy();
|
||||
expect(permissionBoundary.missingPermissions).toEqual(expect.arrayContaining(missingPermissions));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validate Permission Boundary: Checking Parent $eq operator", () => {
|
||||
const parentPermission = createMongoAbility([
|
||||
{
|
||||
action: ["create", "read"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$NEQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$GLOB]: "staging" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validate Permission Boundary: Checking Parent $neq operator", () => {
|
||||
const parentPermission = createMongoAbility([
|
||||
{
|
||||
action: ["create", "read"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$NEQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/staging"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/dev**" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "/hello" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$NEQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$NEQ]: "/" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validate Permission Boundary: Checking Parent $IN operator", () => {
|
||||
const parentPermission = createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: `${PermissionConditionOperators.$IN} - 2`,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$NEQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$NEQ]: "dev" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["edit"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validate Permission Boundary: Checking Parent $GLOB operator", () => {
|
||||
const parentPermission = createMongoAbility([
|
||||
{
|
||||
action: ["create", "read"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$IN]: ["/hello/world", "/hello/world2"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**/world" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$EQ]: "/print" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$NEQ,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello/world" }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$IN,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
operator: PermissionConditionOperators.$GLOB,
|
||||
childPermission: createMongoAbility([
|
||||
{
|
||||
action: ["create"],
|
||||
subject: "secrets",
|
||||
conditions: {
|
||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||
expect(permissionBoundary.isValid).toBeFalsy();
|
||||
});
|
||||
});
|
249
backend/src/lib/casl/boundary.ts
Normal file
249
backend/src/lib/casl/boundary.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import { MongoAbility } from "@casl/ability";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import picomatch from "picomatch";
|
||||
|
||||
import { PermissionConditionOperators } from "./index";
|
||||
|
||||
type TMissingPermission = {
|
||||
action: string;
|
||||
subject: string;
|
||||
conditions?: MongoQuery;
|
||||
};
|
||||
|
||||
type TPermissionConditionShape = {
|
||||
[PermissionConditionOperators.$EQ]: string;
|
||||
[PermissionConditionOperators.$NEQ]: string;
|
||||
[PermissionConditionOperators.$GLOB]: string;
|
||||
[PermissionConditionOperators.$IN]: string[];
|
||||
};
|
||||
|
||||
const getPermissionSetID = (action: string, subject: string) => `${action}:${subject}`;
|
||||
const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation);
|
||||
const formatConditionOperator = (condition: TPermissionConditionShape | string) => {
|
||||
return (
|
||||
typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition
|
||||
) as TPermissionConditionShape;
|
||||
};
|
||||
|
||||
const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => {
|
||||
// we compute each operator against each other in left hand side and right hand side
|
||||
if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) {
|
||||
const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ];
|
||||
const isInverted = !subset[PermissionConditionOperators.$EQ];
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$EQ] &&
|
||||
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$IN] &&
|
||||
invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// ne and glob cannot match each other
|
||||
if (parentSet[PermissionConditionOperators.$GLOB] && isInverted) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false })
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (subset[PermissionConditionOperators.$IN]) {
|
||||
const subsetOperatorValue = subset[PermissionConditionOperators.$IN];
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$EQ] &&
|
||||
(subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||
subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$IN] &&
|
||||
!subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||
!subsetOperatorValue.every((el) =>
|
||||
picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], {
|
||||
strictSlashes: false
|
||||
})
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (subset[PermissionConditionOperators.$GLOB]) {
|
||||
const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB];
|
||||
const { isGlob } = picomatch.scan(subsetOperatorValue);
|
||||
// if it's glob, all other fixed operators would make this superset because glob is powerful. like eq
|
||||
// example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger
|
||||
if (
|
||||
isGlob &&
|
||||
Object.keys(parentSet).some(
|
||||
(el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$EQ] &&
|
||||
parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||
picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, {
|
||||
strictSlashes: false
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// if parent set is IN, glob cannot be used for children - It's a bigger scope
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$IN] &&
|
||||
!parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], {
|
||||
strictSlashes: false
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isSubsetForSamePermissionSubjectAction = (
|
||||
parentSetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||
subsetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||
appendToMissingPermission: (condition?: MongoQuery) => void
|
||||
) => {
|
||||
const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions);
|
||||
if (isMissingConditionInParent) return true;
|
||||
|
||||
// all subset rules must pass in comparison to parent rul
|
||||
return subsetRules.every((subsetRule) => {
|
||||
const subsetRuleConditions = subsetRule.conditions as Record<string, TPermissionConditionShape | string>;
|
||||
// compare subset rule with all parent rules
|
||||
const isSubsetOfNonInvertedParentSet = parentSetRules
|
||||
.filter((el) => !el.inverted)
|
||||
.some((parentSetRule) => {
|
||||
// get conditions and iterate
|
||||
const parentSetRuleConditions = parentSetRule?.conditions as Record<string, TPermissionConditionShape | string>;
|
||||
if (!parentSetRuleConditions) return true;
|
||||
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||
// if parent condition is missing then it's never a subset
|
||||
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||
|
||||
// standardize the conditions plain string operator => $eq function
|
||||
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||
});
|
||||
});
|
||||
|
||||
const invertedParentSetRules = parentSetRules.filter((el) => el.inverted);
|
||||
const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length
|
||||
? !invertedParentSetRules.some((parentSetRule) => {
|
||||
// get conditions and iterate
|
||||
const parentSetRuleConditions = parentSetRule?.conditions as Record<
|
||||
string,
|
||||
TPermissionConditionShape | string
|
||||
>;
|
||||
if (!parentSetRuleConditions) return true;
|
||||
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||
// if parent condition is missing then it's never a subset
|
||||
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||
|
||||
// standardize the conditions plain string operator => $eq function
|
||||
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||
});
|
||||
})
|
||||
: true;
|
||||
const isSubset = isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet;
|
||||
if (!isSubset) {
|
||||
appendToMissingPermission(subsetRule.conditions);
|
||||
}
|
||||
return isSubset;
|
||||
});
|
||||
};
|
||||
|
||||
export const validatePermissionBoundary = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => {
|
||||
const checkedPermissionRules = new Set<string>();
|
||||
const missingPermissions: TMissingPermission[] = [];
|
||||
|
||||
subsetPermissions.rules.forEach((subsetPermissionRules) => {
|
||||
const subsetPermissionSubject = subsetPermissionRules.subject.toString();
|
||||
let subsetPermissionActions: string[] = [];
|
||||
|
||||
// actions can be string or string[]
|
||||
if (typeof subsetPermissionRules.action === "string") {
|
||||
subsetPermissionActions.push(subsetPermissionRules.action);
|
||||
} else {
|
||||
subsetPermissionRules.action.forEach((subsetPermissionAction) => {
|
||||
subsetPermissionActions.push(subsetPermissionAction);
|
||||
});
|
||||
}
|
||||
|
||||
// if action is already processed ignore
|
||||
subsetPermissionActions = subsetPermissionActions.filter(
|
||||
(el) => !checkedPermissionRules.has(getPermissionSetID(el, subsetPermissionSubject))
|
||||
);
|
||||
|
||||
if (!subsetPermissionActions.length) return;
|
||||
subsetPermissionActions.forEach((subsetPermissionAction) => {
|
||||
const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor(
|
||||
subsetPermissionAction,
|
||||
subsetPermissionSubject
|
||||
);
|
||||
const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted);
|
||||
if (!nonInveretedOnes.length) {
|
||||
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject });
|
||||
return;
|
||||
}
|
||||
|
||||
const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject);
|
||||
isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules, (conditions) => {
|
||||
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject, conditions });
|
||||
});
|
||||
});
|
||||
|
||||
subsetPermissionActions.forEach((el) =>
|
||||
checkedPermissionRules.add(getPermissionSetID(el, subsetPermissionSubject))
|
||||
);
|
||||
});
|
||||
|
||||
if (missingPermissions.length) {
|
||||
return { isValid: false as const, missingPermissions };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability";
|
||||
import { buildMongoQueryMatcher } from "@casl/ability";
|
||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||
import picomatch from "picomatch";
|
||||
|
||||
@ -20,45 +20,8 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
||||
|
||||
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||
|
||||
/**
|
||||
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
||||
*/
|
||||
const extractPermissions = (ability: MongoAbility) => {
|
||||
const permissions: string[] = [];
|
||||
ability.rules.forEach((permission) => {
|
||||
if (typeof permission.action === "string") {
|
||||
permissions.push(`${permission.action}_${permission.subject as string}`);
|
||||
} else {
|
||||
permission.action.forEach((permissionAction) => {
|
||||
permissions.push(`${permissionAction}_${permission.subject as string}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
return permissions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
||||
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
|
||||
*
|
||||
*/
|
||||
export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => {
|
||||
const set1 = new Set(extractPermissions(permissions1));
|
||||
const set2 = new Set(extractPermissions(permissions2));
|
||||
|
||||
for (const perm of set2) {
|
||||
if (!set1.has(perm)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return set1.size >= set2.size;
|
||||
};
|
||||
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
$REGEX = "$regex",
|
||||
$EQ = "$eq",
|
||||
$NEQ = "$ne",
|
||||
$GLOB = "$glob"
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
export class DatabaseError extends Error {
|
||||
name: string;
|
||||
|
||||
@ -52,10 +53,18 @@ export class ForbiddenRequestError extends Error {
|
||||
|
||||
error: unknown;
|
||||
|
||||
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
|
||||
details?: unknown;
|
||||
|
||||
constructor({
|
||||
name,
|
||||
error,
|
||||
message,
|
||||
details
|
||||
}: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) {
|
||||
super(message ?? "You are not allowed to access this resource");
|
||||
this.name = name || "ForbiddenError";
|
||||
this.error = error;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,7 @@ export const pingGatewayAndVerify = async ({
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||
try {
|
||||
const stream = quicClient.connection.newStream("bidi");
|
||||
@ -108,17 +109,13 @@ export const pingGatewayAndVerify = async ({
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway closed before receiving PONG"
|
||||
});
|
||||
throw new Error("Gateway closed before receiving PONG");
|
||||
}
|
||||
|
||||
const response = Buffer.from(value).toString();
|
||||
|
||||
if (response !== "PONG\n" && response !== "PONG") {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to Ping. Unexpected response: ${response}`
|
||||
});
|
||||
throw new Error(`Failed to Ping. Unexpected response: ${response}`);
|
||||
}
|
||||
|
||||
reader.releaseLock();
|
||||
@ -146,6 +143,7 @@ interface TProxyServer {
|
||||
server: net.Server;
|
||||
port: number;
|
||||
cleanup: () => Promise<void>;
|
||||
getProxyError: () => string;
|
||||
}
|
||||
|
||||
const setupProxyServer = async ({
|
||||
@ -170,6 +168,7 @@ const setupProxyServer = async ({
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
const proxyErrorMsg = [""];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
@ -185,31 +184,33 @@ const setupProxyServer = async ({
|
||||
const forwardWriter = stream.writable.getWriter();
|
||||
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
|
||||
forwardWriter.releaseLock();
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
|
||||
// Set up bidirectional copy
|
||||
const setupCopy = async () => {
|
||||
const setupCopy = () => {
|
||||
// Client to QUIC
|
||||
// eslint-disable-next-line
|
||||
(async () => {
|
||||
try {
|
||||
const writer = stream.writable.getWriter();
|
||||
const writer = stream.writable.getWriter();
|
||||
|
||||
// Create a handler for client data
|
||||
clientConn.on("data", async (chunk) => {
|
||||
await writer.write(chunk);
|
||||
// Create a handler for client data
|
||||
clientConn.on("data", (chunk) => {
|
||||
writer.write(chunk).catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle client connection close
|
||||
clientConn.on("end", async () => {
|
||||
await writer.close();
|
||||
// Handle client connection close
|
||||
clientConn.on("end", () => {
|
||||
writer.close().catch((err) => {
|
||||
logger.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
clientConn.on("error", async (err) => {
|
||||
await writer.abort(err);
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
writer.abort(clientConnErr?.message).catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
} catch (err) {
|
||||
clientConn.destroy();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// QUIC to Client
|
||||
@ -238,15 +239,18 @@ const setupProxyServer = async ({
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
clientConn.destroy();
|
||||
}
|
||||
})();
|
||||
};
|
||||
await setupCopy();
|
||||
//
|
||||
|
||||
setupCopy();
|
||||
// Handle connection closure
|
||||
clientConn.on("close", async () => {
|
||||
await stream.destroy();
|
||||
clientConn.on("close", () => {
|
||||
stream.destroy().catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
@ -254,13 +258,18 @@ const setupProxyServer = async ({
|
||||
await stream.destroy();
|
||||
};
|
||||
|
||||
clientConn.on("error", (err) => {
|
||||
logger.error(err, "Client socket error");
|
||||
void cleanup();
|
||||
reject(err);
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
logger.error(clientConnErr, "Client socket error");
|
||||
cleanup().catch((err) => {
|
||||
logger.error(err, "Client conn cleanup");
|
||||
});
|
||||
});
|
||||
|
||||
clientConn.on("end", cleanup);
|
||||
clientConn.on("end", () => {
|
||||
cleanup().catch((err) => {
|
||||
logger.error(err, "Client conn end");
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to establish target connection:");
|
||||
clientConn.end();
|
||||
@ -272,12 +281,12 @@ const setupProxyServer = async ({
|
||||
reject(err);
|
||||
});
|
||||
|
||||
server.on("close", async () => {
|
||||
await quicClient?.destroy();
|
||||
server.on("close", () => {
|
||||
quicClient?.destroy().catch((err) => {
|
||||
logger.error(err, "Failed to destroy quic client");
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
@ -293,7 +302,8 @@ const setupProxyServer = async ({
|
||||
cleanup: async () => {
|
||||
server.close();
|
||||
await quicClient?.destroy();
|
||||
}
|
||||
},
|
||||
getProxyError: () => proxyErrorMsg.join(",")
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -316,7 +326,7 @@ export const withGatewayProxy = async (
|
||||
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
|
||||
|
||||
// Setup the proxy server
|
||||
const { port, cleanup } = await setupProxyServer({
|
||||
const { port, cleanup, getProxyError } = await setupProxyServer({
|
||||
targetHost,
|
||||
targetPort,
|
||||
relayPort,
|
||||
@ -330,8 +340,12 @@ export const withGatewayProxy = async (
|
||||
// Execute the callback with the allocated port
|
||||
await callback(port);
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to proxy");
|
||||
throw new BadRequestError({ message: (err as Error)?.message });
|
||||
const proxyErrorMessage = getProxyError();
|
||||
if (proxyErrorMessage) {
|
||||
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
|
||||
}
|
||||
logger.error(err, "Failed to do gateway");
|
||||
throw new BadRequestError({ message: proxyErrorMessage || (err as Error)?.message });
|
||||
} finally {
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
await cleanup();
|
||||
|
15
backend/src/lib/ms/index.ts
Normal file
15
backend/src/lib/ms/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import msFn, { StringValue } from "ms";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
|
||||
export const ms = (val: string) => {
|
||||
if (typeof val !== "string") {
|
||||
throw new BadRequestError({ message: `Date must be string` });
|
||||
}
|
||||
|
||||
try {
|
||||
return msFn(val as StringValue);
|
||||
} catch {
|
||||
throw new BadRequestError({ message: `Invalid date format string: ${val}` });
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
const TURN_TOKEN_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
|
||||
const timestamp = Math.floor((Date.now() + ttl) / 1000);
|
||||
const username = `${timestamp}:${id}`;
|
||||
|
@ -83,6 +83,14 @@ const run = async () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(error, "CRITICAL ERROR: Uncaught Exception");
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (error) => {
|
||||
logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection");
|
||||
});
|
||||
|
||||
await server.listen({
|
||||
port: envConfig.PORT,
|
||||
host: envConfig.HOST,
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
TQueueSecretSyncSyncSecretsByIdDTO,
|
||||
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||
|
||||
export enum QueueName {
|
||||
SecretRotation = "secret-rotation",
|
||||
@ -107,7 +108,7 @@ export type TQueueJobTypes = {
|
||||
};
|
||||
[QueueName.SecretWebhook]: {
|
||||
name: QueueJobs.SecWebhook;
|
||||
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||
payload: TWebhookPayloads;
|
||||
};
|
||||
|
||||
[QueueName.AccessTokenStatusUpdate]:
|
||||
|
@ -21,3 +21,10 @@ export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInpu
|
||||
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
||||
});
|
||||
};
|
||||
|
||||
export const GenericResourceNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Name must be at least 1 character" })
|
||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
||||
|
@ -122,7 +122,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
reqId: req.id,
|
||||
statusCode: HttpStatusCodes.Forbidden,
|
||||
message: error.message,
|
||||
error: error.name
|
||||
error: error.name,
|
||||
details: error?.details
|
||||
});
|
||||
} else if (error instanceof RateLimitError) {
|
||||
void res.status(HttpStatusCodes.TooManyRequests).send({
|
||||
|
@ -75,6 +75,8 @@ import { secretReplicationServiceFactory } from "@app/ee/services/secret-replica
|
||||
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||
import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||
import { secretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal";
|
||||
import { secretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||
import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal";
|
||||
import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal";
|
||||
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
|
||||
@ -401,6 +403,8 @@ export const registerRoutes = async (
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
|
||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@ -635,6 +639,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
const superAdminService = superAdminServiceFactory({
|
||||
userDAL,
|
||||
identityDAL,
|
||||
userAliasDAL,
|
||||
authService: loginService,
|
||||
serverCfgDAL: superAdminDAL,
|
||||
@ -1481,6 +1486,15 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const secretRotationV2Service = secretRotationV2ServiceFactory({
|
||||
secretRotationV2DAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
folderDAL,
|
||||
projectBotService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
|
||||
// setup the communication with license key server
|
||||
@ -1582,7 +1596,8 @@ export const registerRoutes = async (
|
||||
secretSync: secretSyncService,
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService
|
||||
gateway: gatewayService,
|
||||
secretRotationV2: secretRotationV2Service
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
ProjectRolesSchema,
|
||||
ProjectsSchema,
|
||||
SecretApprovalPoliciesSchema,
|
||||
SecretTagsSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@ -111,7 +112,16 @@ export const secretRawSchema = z.object({
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
actor: z
|
||||
.object({
|
||||
actorId: z.string().nullable().optional(),
|
||||
actorType: z.string().nullable().optional(),
|
||||
name: z.string().nullable().optional(),
|
||||
membershipId: z.string().nullable().optional()
|
||||
})
|
||||
.optional()
|
||||
.nullable()
|
||||
});
|
||||
|
||||
export const ProjectPermissionSchema = z.object({
|
||||
@ -232,3 +242,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
||||
kmsCertificateKeyId: true,
|
||||
auditLogsRetentionDays: true
|
||||
});
|
||||
|
||||
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
}).extend({
|
||||
name: z.string()
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
querystring: z.object({
|
||||
searchTerm: z.string().default(""),
|
||||
offset: z.coerce.number().default(0),
|
||||
limit: z.coerce.number().max(100).default(20)
|
||||
limit: z.coerce.number().max(100).default(20),
|
||||
// TODO: remove this once z.coerce.boolean() is supported
|
||||
adminsOnly: z
|
||||
.string()
|
||||
.transform((val) => val === "true")
|
||||
.default("false")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -149,6 +154,43 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/identity-management/identities",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
searchTerm: z.string().default(""),
|
||||
offset: z.coerce.number().default(0),
|
||||
limit: z.coerce.number().max(100).default(20)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identities: IdentitiesSchema.pick({
|
||||
name: true,
|
||||
id: true
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, res, done) => {
|
||||
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||
verifySuperAdmin(req, res, done);
|
||||
});
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identities = await server.services.superAdmin.getIdentities({
|
||||
...req.query
|
||||
});
|
||||
|
||||
return {
|
||||
identities
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/integrations/slack/config",
|
||||
|
@ -24,8 +24,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
method: I["method"];
|
||||
credentials: I["credentials"];
|
||||
description?: string | null;
|
||||
isPlatformManaged?: boolean;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
name?: string;
|
||||
credentials?: I["credentials"];
|
||||
description?: string | null;
|
||||
isPlatformManaged?: boolean;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||
sanitizedResponseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||
@ -208,10 +214,10 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, method, credentials, description } = req.body;
|
||||
const { name, method, credentials, description, isPlatformManaged } = req.body;
|
||||
|
||||
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||
{ name, method, app, credentials, description },
|
||||
{ name, method, app, credentials, description, isPlatformManaged },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
@ -224,7 +230,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
name,
|
||||
method,
|
||||
app,
|
||||
connectionId: appConnection.id
|
||||
connectionId: appConnection.id,
|
||||
isPlatformManaged
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -251,11 +258,11 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, credentials, description } = req.body;
|
||||
const { name, credentials, description, isPlatformManaged } = req.body;
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const appConnection = (await server.services.appConnection.updateAppConnection(
|
||||
{ name, credentials, connectionId, description },
|
||||
{ name, credentials, connectionId, description, isPlatformManaged },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
@ -268,7 +275,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
||||
name,
|
||||
description,
|
||||
credentialsUpdated: Boolean(credentials),
|
||||
connectionId
|
||||
connectionId,
|
||||
isPlatformManaged
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -18,6 +18,15 @@ import {
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
import {
|
||||
HumanitecConnectionListItemSchema,
|
||||
SanitizedHumanitecConnectionSchema
|
||||
} from "@app/services/app-connection/humanitec";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import {
|
||||
PostgresConnectionListItemSchema,
|
||||
SanitizedPostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
@ -27,7 +36,10 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedGcpConnectionSchema.options,
|
||||
...SanitizedAzureKeyVaultConnectionSchema.options,
|
||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||
...SanitizedDatabricksConnectionSchema.options
|
||||
...SanitizedDatabricksConnectionSchema.options,
|
||||
...SanitizedHumanitecConnectionSchema.options,
|
||||
...SanitizedPostgresConnectionSchema.options,
|
||||
...SanitizedMsSqlConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@ -36,7 +48,10 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
GcpConnectionListItemSchema,
|
||||
AzureKeyVaultConnectionListItemSchema,
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
DatabricksConnectionListItemSchema
|
||||
DatabricksConnectionListItemSchema,
|
||||
HumanitecConnectionListItemSchema,
|
||||
PostgresConnectionListItemSchema,
|
||||
MsSqlConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
@ -0,0 +1,69 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateHumanitecConnectionSchema,
|
||||
HumanitecOrgWithApps,
|
||||
SanitizedHumanitecConnectionSchema,
|
||||
UpdateHumanitecConnectionSchema
|
||||
} from "@app/services/app-connection/humanitec";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerHumanitecConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Humanitec,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedHumanitecConnectionSchema,
|
||||
createSchema: CreateHumanitecConnectionSchema,
|
||||
updateSchema: UpdateHumanitecConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/organizations`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
apps: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
envs: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const organizations: HumanitecOrgWithApps[] = await server.services.appConnection.humanitec.listOrganizations(
|
||||
connectionId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return organizations;
|
||||
}
|
||||
});
|
||||
};
|
@ -6,6 +6,9 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
|
||||
export * from "./app-connection-router";
|
||||
|
||||
@ -16,5 +19,8 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.GCP]: registerGcpConnectionRouter,
|
||||
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter
|
||||
};
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateMsSqlConnectionSchema,
|
||||
SanitizedMsSqlConnectionSchema,
|
||||
UpdateMsSqlConnectionSchema
|
||||
} from "@app/services/app-connection/mssql";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerMsSqlConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.MsSql,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedMsSqlConnectionSchema,
|
||||
createSchema: CreateMsSqlConnectionSchema,
|
||||
updateSchema: UpdateMsSqlConnectionSchema
|
||||
});
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreatePostgresConnectionSchema,
|
||||
SanitizedPostgresConnectionSchema,
|
||||
UpdatePostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerPostgresConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Postgres,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedPostgresConnectionSchema,
|
||||
createSchema: CreatePostgresConnectionSchema,
|
||||
updateSchema: UpdatePostgresConnectionSchema
|
||||
});
|
||||
};
|
@ -1,10 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
||||
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { DASHBOARD } from "@app/lib/api-docs";
|
||||
@ -15,7 +16,7 @@ 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 { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
@ -116,16 +117,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
@ -294,6 +289,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
||||
viewSecretValue: true,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
@ -393,6 +389,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.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(),
|
||||
viewSecretValue: booleanSchema.default(true),
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||
@ -410,16 +407,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
@ -601,23 +592,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
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;
|
||||
secrets = (
|
||||
await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
viewSecretValue: req.query.viewSecretValue,
|
||||
throwOnMissingReadValuePermission: false,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
tagSlugs: tags
|
||||
})
|
||||
).secrets;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
@ -696,16 +689,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
@ -749,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
const secrets = await server.services.secret.getSecretsRawByFolderMappings(
|
||||
{
|
||||
filterByAction: ProjectPermissionSecretActions.DescribeSecret,
|
||||
projectId,
|
||||
folderMappings,
|
||||
filters: {
|
||||
@ -846,6 +834,52 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/accessible-secrets",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
filterByAction: z
|
||||
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
|
||||
.default(ProjectPermissionSecretActions.ReadValue)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretPath: z.string().optional(),
|
||||
secretValueHidden: z.boolean()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { projectId, environment, secretPath, filterByAction } = req.query;
|
||||
|
||||
const { secrets } = await server.services.secret.getAccessibleSecrets({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
secretPath,
|
||||
projectId,
|
||||
filterByAction
|
||||
});
|
||||
|
||||
return { secrets };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secrets-by-keys",
|
||||
@ -862,22 +896,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
keys: z.string().trim().transform(decodeURIComponent)
|
||||
keys: z.string().trim().transform(decodeURIComponent),
|
||||
viewSecretValue: booleanSchema.default(false)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
@ -886,7 +915,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretPath, projectId, environment } = req.query;
|
||||
const { secretPath, projectId, environment, viewSecretValue } = req.query;
|
||||
|
||||
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
|
||||
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
|
||||
@ -895,6 +924,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
viewSecretValue,
|
||||
environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
|
@ -91,7 +91,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await projectRouter.register(registerProjectMembershipRouter);
|
||||
await projectRouter.register(registerSecretTagRouter);
|
||||
},
|
||||
|
||||
{ prefix: "/workspace" }
|
||||
);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user