mirror of
https://github.com/Infisical/infisical.git
synced 2025-06-29 04:31:59 +00:00
Compare commits
217 Commits
fix/resolv
...
daniel/red
Author | SHA1 | Date | |
---|---|---|---|
c59a53180c | |||
f56d265e62 | |||
cc0ff98d4f | |||
4a14c3efd2 | |||
b2d2297914 | |||
836bb6d835 | |||
177eb2afee | |||
594df18611 | |||
3bcb8bf6fc | |||
a74c37c18b | |||
3ece81d663 | |||
f6d87ebf32 | |||
23483ab7e1 | |||
fe31d44d22 | |||
58bab4d163 | |||
8f48a64fd6 | |||
929dc059c3 | |||
7c540b6be8 | |||
b975996158 | |||
122f789cdf | |||
c9911aa841 | |||
32cd0d8af8 | |||
585f0d9f1b | |||
d0292aa139 | |||
4e9be8ca3c | |||
ad49e9eaf1 | |||
fed60f7c03 | |||
1bc0e3087a | |||
80a4f838a1 | |||
d31ec44f50 | |||
d0caef37ce | |||
2d26febe58 | |||
c23ad8ebf2 | |||
bad068ef19 | |||
53430608a8 | |||
b9071ab2b3 | |||
a556c02df6 | |||
bfab270d68 | |||
8ea6a1f3d5 | |||
3c39bf6a0f | |||
828644799f | |||
411e67ae41 | |||
4914bc4b5a | |||
d7050a1947 | |||
3c59422511 | |||
c81204e6d5 | |||
880f39519f | |||
8646f6c50b | |||
437a9e6ccb | |||
b54139bd37 | |||
8a6a36ac54 | |||
c6eb973da0 | |||
21750a8c20 | |||
a598665b2f | |||
56bbf502a2 | |||
9975f7d83f | |||
7ad366b363 | |||
cca4d68d94 | |||
b82b94db54 | |||
de9cb265e0 | |||
5611b9aba1 | |||
53075d503a | |||
e47cfa262a | |||
0ab7a4e713 | |||
5138d588db | |||
7e2d093e29 | |||
2d780e0566 | |||
7ac4ad3194 | |||
3ab6eb62c8 | |||
8eb234a12f | |||
85590af99e | |||
5c7cec0c81 | |||
68f768749b | |||
2c7e342b18 | |||
632900e516 | |||
5fd975b1d7 | |||
d45ac66064 | |||
47cba8ec3c | |||
d4aab66da2 | |||
0dc4c92c89 | |||
f49c963367 | |||
fe11b8e57e | |||
79680b6a73 | |||
58838c541f | |||
03cc71cfed | |||
02529106c9 | |||
0401f55bc3 | |||
403e0d2d9d | |||
d939ff289d | |||
d1816c3051 | |||
cb350788c0 | |||
cd58768d6f | |||
dcd6f4d55d | |||
3c828614b8 | |||
09e7988596 | |||
f40df19334 | |||
76c9d3488b | |||
0809da33e0 | |||
b528eec4bb | |||
5179103680 | |||
25a9e5f58a | |||
8ddfe7b6e9 | |||
c23f21d57a | |||
1242a43d98 | |||
1655ca27d1 | |||
2bcead03b0 | |||
41ab1972ce | |||
b00fff6922 | |||
97b01ca5f8 | |||
c2bd6f5ef3 | |||
18efc9a6de | |||
436ccb25fb | |||
8f08a352dd | |||
00f86cfd00 | |||
3944aafb11 | |||
a6b852fab9 | |||
2a043afe11 | |||
df8f2cf9ab | |||
a18015b1e5 | |||
8b80622d2f | |||
c0fd0a56f3 | |||
326764dd41 | |||
1f24d02c5e | |||
c130fbddd9 | |||
f560534493 | |||
10a97f4522 | |||
7a2f0214f3 | |||
a2b994ab23 | |||
e73d3f87f3 | |||
c4715124dc | |||
b53607f8e4 | |||
8f79d3210a | |||
68b1984a76 | |||
ba45e83880 | |||
28ecc37163 | |||
a6a2e2bae0 | |||
d8bbfacae0 | |||
58549c398f | |||
842ed62bec | |||
06d8800ee0 | |||
2ecfd1bb7e | |||
783d4c7bd6 | |||
fbf3f26abd | |||
1d09693041 | |||
626e37e3d0 | |||
07fd67b328 | |||
3f1f018adc | |||
fe04e6d20c | |||
d7171a1617 | |||
384a0daa31 | |||
c5c949e034 | |||
c2c9edf156 | |||
c8248ef4e9 | |||
9f6a6a7b7c | |||
121b642d50 | |||
59b16f647e | |||
2ab5932693 | |||
8dfcef3900 | |||
8ca70eec44 | |||
60df59c7f0 | |||
e231c531a6 | |||
d48bb910fa | |||
1317266415 | |||
288f47f4bd | |||
b090ebfd41 | |||
67773bff5e | |||
8ef1cfda04 | |||
2a79d5ba36 | |||
0cb95f36ff | |||
288d7e88ae | |||
f88389bf9e | |||
2e88c5e2c5 | |||
73f3b8173e | |||
aa5b88ff04 | |||
b7caff88cf | |||
760a1e917a | |||
2d7ff66246 | |||
179497e830 | |||
4c08c80e5b | |||
7d6af64904 | |||
16519f9486 | |||
bb27d38a12 | |||
5b26928751 | |||
f425e7e48f | |||
4601f46afb | |||
692bdc060c | |||
3a4f8c2e54 | |||
146c4284a2 | |||
5ae33b9f3b | |||
1f38b92ec6 | |||
f2a49a79f0 | |||
3ddb4cd27a | |||
a5555c3816 | |||
8479c406a5 | |||
8e0b4254b1 | |||
069651bdb4 | |||
9061ec2dff | |||
b0a5023723 | |||
69fe5bf71d | |||
f12d4d80c6 | |||
56f2a3afa4 | |||
406da1b5f0 | |||
da45e132a3 | |||
fb719a9383 | |||
3c64359597 | |||
e420973dd2 | |||
15cc157c5f | |||
ad89ffe94d | |||
4de1713a18 | |||
1917e0fdb7 | |||
4b07234997 | |||
6a402950c3 | |||
63333159ca | |||
ce4ba24ef2 | |||
f606e31b98 | |||
ecdbb3eb53 | |||
0321ec32fb |
@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
392
backend/package-lock.json
generated
392
backend/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-kms": "^3.609.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
@ -29,7 +30,7 @@
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@peculiar/x509": "^1.10.0",
|
||||
"@peculiar/x509": "^1.12.1",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@sindresorhus/slugify": "1.1.0",
|
||||
"@team-plain/typescript-sdk": "^4.6.1",
|
||||
@ -74,9 +75,12 @@
|
||||
"pg-query-stream": "^4.5.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"pkijs": "^3.2.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"scim-patch": "^0.8.3",
|
||||
"scim2-parse-filter": "^0.2.10",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@ -351,6 +355,309 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.637.0.tgz",
|
||||
"integrity": "sha512-e54OYm33DqmcsVHr1l+Eudt5d9PqcjDDJdQHLJrNGdrUkwmpuqnw3czkGjD5IP34XkcpQ5Gs1DSRAp07E8Zglw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/client-sso-oidc": "3.637.0",
|
||||
"@aws-sdk/client-sts": "3.637.0",
|
||||
"@aws-sdk/core": "3.635.0",
|
||||
"@aws-sdk/credential-provider-node": "3.637.0",
|
||||
"@aws-sdk/middleware-host-header": "3.620.0",
|
||||
"@aws-sdk/middleware-logger": "3.609.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.620.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.637.0",
|
||||
"@aws-sdk/region-config-resolver": "3.614.0",
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@aws-sdk/util-endpoints": "3.637.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.609.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.614.0",
|
||||
"@smithy/config-resolver": "^3.0.5",
|
||||
"@smithy/core": "^2.4.0",
|
||||
"@smithy/fetch-http-handler": "^3.2.4",
|
||||
"@smithy/hash-node": "^3.0.3",
|
||||
"@smithy/invalid-dependency": "^3.0.3",
|
||||
"@smithy/middleware-content-length": "^3.0.5",
|
||||
"@smithy/middleware-endpoint": "^3.1.0",
|
||||
"@smithy/middleware-retry": "^3.0.15",
|
||||
"@smithy/middleware-serde": "^3.0.3",
|
||||
"@smithy/middleware-stack": "^3.0.3",
|
||||
"@smithy/node-config-provider": "^3.1.4",
|
||||
"@smithy/node-http-handler": "^3.1.4",
|
||||
"@smithy/protocol-http": "^4.1.0",
|
||||
"@smithy/smithy-client": "^3.2.0",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"@smithy/url-parser": "^3.0.3",
|
||||
"@smithy/util-base64": "^3.0.0",
|
||||
"@smithy/util-body-length-browser": "^3.0.0",
|
||||
"@smithy/util-body-length-node": "^3.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^3.0.15",
|
||||
"@smithy/util-defaults-mode-node": "^3.0.15",
|
||||
"@smithy/util-endpoints": "^2.0.5",
|
||||
"@smithy/util-middleware": "^3.0.3",
|
||||
"@smithy/util-retry": "^3.0.3",
|
||||
"@smithy/util-utf8": "^3.0.0",
|
||||
"@smithy/util-waiter": "^3.1.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/client-sso": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.637.0.tgz",
|
||||
"integrity": "sha512-+KjLvgX5yJYROWo3TQuwBJlHCY0zz9PsLuEolmXQn0BVK1L/m9GteZHtd+rEdAoDGBpE0Xqjy1oz5+SmtsaRUw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.635.0",
|
||||
"@aws-sdk/middleware-host-header": "3.620.0",
|
||||
"@aws-sdk/middleware-logger": "3.609.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.620.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.637.0",
|
||||
"@aws-sdk/region-config-resolver": "3.614.0",
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@aws-sdk/util-endpoints": "3.637.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.609.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.614.0",
|
||||
"@smithy/config-resolver": "^3.0.5",
|
||||
"@smithy/core": "^2.4.0",
|
||||
"@smithy/fetch-http-handler": "^3.2.4",
|
||||
"@smithy/hash-node": "^3.0.3",
|
||||
"@smithy/invalid-dependency": "^3.0.3",
|
||||
"@smithy/middleware-content-length": "^3.0.5",
|
||||
"@smithy/middleware-endpoint": "^3.1.0",
|
||||
"@smithy/middleware-retry": "^3.0.15",
|
||||
"@smithy/middleware-serde": "^3.0.3",
|
||||
"@smithy/middleware-stack": "^3.0.3",
|
||||
"@smithy/node-config-provider": "^3.1.4",
|
||||
"@smithy/node-http-handler": "^3.1.4",
|
||||
"@smithy/protocol-http": "^4.1.0",
|
||||
"@smithy/smithy-client": "^3.2.0",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"@smithy/url-parser": "^3.0.3",
|
||||
"@smithy/util-base64": "^3.0.0",
|
||||
"@smithy/util-body-length-browser": "^3.0.0",
|
||||
"@smithy/util-body-length-node": "^3.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^3.0.15",
|
||||
"@smithy/util-defaults-mode-node": "^3.0.15",
|
||||
"@smithy/util-endpoints": "^2.0.5",
|
||||
"@smithy/util-middleware": "^3.0.3",
|
||||
"@smithy/util-retry": "^3.0.3",
|
||||
"@smithy/util-utf8": "^3.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/client-sso-oidc": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.637.0.tgz",
|
||||
"integrity": "sha512-27bHALN6Qb6m6KZmPvRieJ/QRlj1lyac/GT2Rn5kJpre8Mpp+yxrtvp3h9PjNBty4lCeFEENfY4dGNSozBuBcw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.635.0",
|
||||
"@aws-sdk/credential-provider-node": "3.637.0",
|
||||
"@aws-sdk/middleware-host-header": "3.620.0",
|
||||
"@aws-sdk/middleware-logger": "3.609.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.620.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.637.0",
|
||||
"@aws-sdk/region-config-resolver": "3.614.0",
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@aws-sdk/util-endpoints": "3.637.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.609.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.614.0",
|
||||
"@smithy/config-resolver": "^3.0.5",
|
||||
"@smithy/core": "^2.4.0",
|
||||
"@smithy/fetch-http-handler": "^3.2.4",
|
||||
"@smithy/hash-node": "^3.0.3",
|
||||
"@smithy/invalid-dependency": "^3.0.3",
|
||||
"@smithy/middleware-content-length": "^3.0.5",
|
||||
"@smithy/middleware-endpoint": "^3.1.0",
|
||||
"@smithy/middleware-retry": "^3.0.15",
|
||||
"@smithy/middleware-serde": "^3.0.3",
|
||||
"@smithy/middleware-stack": "^3.0.3",
|
||||
"@smithy/node-config-provider": "^3.1.4",
|
||||
"@smithy/node-http-handler": "^3.1.4",
|
||||
"@smithy/protocol-http": "^4.1.0",
|
||||
"@smithy/smithy-client": "^3.2.0",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"@smithy/url-parser": "^3.0.3",
|
||||
"@smithy/util-base64": "^3.0.0",
|
||||
"@smithy/util-body-length-browser": "^3.0.0",
|
||||
"@smithy/util-body-length-node": "^3.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^3.0.15",
|
||||
"@smithy/util-defaults-mode-node": "^3.0.15",
|
||||
"@smithy/util-endpoints": "^2.0.5",
|
||||
"@smithy/util-middleware": "^3.0.3",
|
||||
"@smithy/util-retry": "^3.0.3",
|
||||
"@smithy/util-utf8": "^3.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-sts": "^3.637.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/client-sts": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.637.0.tgz",
|
||||
"integrity": "sha512-xUi7x4qDubtA8QREtlblPuAcn91GS/09YVEY/RwU7xCY0aqGuFwgszAANlha4OUIqva8oVj2WO4gJuG+iaSnhw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/client-sso-oidc": "3.637.0",
|
||||
"@aws-sdk/core": "3.635.0",
|
||||
"@aws-sdk/credential-provider-node": "3.637.0",
|
||||
"@aws-sdk/middleware-host-header": "3.620.0",
|
||||
"@aws-sdk/middleware-logger": "3.609.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.620.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.637.0",
|
||||
"@aws-sdk/region-config-resolver": "3.614.0",
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@aws-sdk/util-endpoints": "3.637.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.609.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.614.0",
|
||||
"@smithy/config-resolver": "^3.0.5",
|
||||
"@smithy/core": "^2.4.0",
|
||||
"@smithy/fetch-http-handler": "^3.2.4",
|
||||
"@smithy/hash-node": "^3.0.3",
|
||||
"@smithy/invalid-dependency": "^3.0.3",
|
||||
"@smithy/middleware-content-length": "^3.0.5",
|
||||
"@smithy/middleware-endpoint": "^3.1.0",
|
||||
"@smithy/middleware-retry": "^3.0.15",
|
||||
"@smithy/middleware-serde": "^3.0.3",
|
||||
"@smithy/middleware-stack": "^3.0.3",
|
||||
"@smithy/node-config-provider": "^3.1.4",
|
||||
"@smithy/node-http-handler": "^3.1.4",
|
||||
"@smithy/protocol-http": "^4.1.0",
|
||||
"@smithy/smithy-client": "^3.2.0",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"@smithy/url-parser": "^3.0.3",
|
||||
"@smithy/util-base64": "^3.0.0",
|
||||
"@smithy/util-body-length-browser": "^3.0.0",
|
||||
"@smithy/util-body-length-node": "^3.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^3.0.15",
|
||||
"@smithy/util-defaults-mode-node": "^3.0.15",
|
||||
"@smithy/util-endpoints": "^2.0.5",
|
||||
"@smithy/util-middleware": "^3.0.3",
|
||||
"@smithy/util-retry": "^3.0.3",
|
||||
"@smithy/util-utf8": "^3.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.637.0.tgz",
|
||||
"integrity": "sha512-h+PFCWfZ0Q3Dx84SppET/TFpcQHmxFW8/oV9ArEvMilw4EBN+IlxgbL0CnHwjHW64szcmrM0mbebjEfHf4FXmw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "3.620.1",
|
||||
"@aws-sdk/credential-provider-http": "3.635.0",
|
||||
"@aws-sdk/credential-provider-process": "3.620.1",
|
||||
"@aws-sdk/credential-provider-sso": "3.637.0",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.621.0",
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@smithy/credential-provider-imds": "^3.2.0",
|
||||
"@smithy/property-provider": "^3.1.3",
|
||||
"@smithy/shared-ini-file-loader": "^3.1.4",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-sts": "^3.637.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.637.0.tgz",
|
||||
"integrity": "sha512-yoEhoxJJfs7sPVQ6Is939BDQJZpZCoUgKr/ySse4YKOZ24t4VqgHA6+wV7rYh+7IW24Rd91UTvEzSuHYTlxlNA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "3.620.1",
|
||||
"@aws-sdk/credential-provider-http": "3.635.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.637.0",
|
||||
"@aws-sdk/credential-provider-process": "3.620.1",
|
||||
"@aws-sdk/credential-provider-sso": "3.637.0",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.621.0",
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@smithy/credential-provider-imds": "^3.2.0",
|
||||
"@smithy/property-provider": "^3.1.3",
|
||||
"@smithy/shared-ini-file-loader": "^3.1.4",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.637.0.tgz",
|
||||
"integrity": "sha512-Mvz+h+e62/tl+dVikLafhv+qkZJ9RUb8l2YN/LeKMWkxQylPT83CPk9aimVhCV89zth1zpREArl97+3xsfgQvA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sso": "3.637.0",
|
||||
"@aws-sdk/token-providers": "3.614.0",
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@smithy/property-provider": "^3.1.3",
|
||||
"@smithy/shared-ini-file-loader": "^3.1.4",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/middleware-user-agent": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.637.0.tgz",
|
||||
"integrity": "sha512-EYo0NE9/da/OY8STDsK2LvM4kNa79DBsf4YVtaG4P5pZ615IeFsD8xOHZeuJmUrSMlVQ8ywPRX7WMucUybsKug==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@aws-sdk/util-endpoints": "3.637.0",
|
||||
"@smithy/protocol-http": "^4.1.0",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/util-endpoints": {
|
||||
"version": "3.637.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.637.0.tgz",
|
||||
"integrity": "sha512-pAqOKUHeVWHEXXDIp/qoMk/6jyxIb6GGjnK1/f8dKHtKIEs4tKsnnL563gceEvdad53OPXIt86uoevCcCzmBnw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "3.609.0",
|
||||
"@smithy/types": "^3.3.0",
|
||||
"@smithy/util-endpoints": "^2.0.5",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-iam": {
|
||||
"version": "3.635.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.635.0.tgz",
|
||||
@ -4457,6 +4764,17 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@ -5029,9 +5347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/x509": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz",
|
||||
"integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.1.tgz",
|
||||
"integrity": "sha512-2T9t2viNP9m20mky50igPTpn2ByhHl5NlT6wW4Tp4BejQaQ5XDNZgfsabYwYysLXhChABlgtTCpp2gM3JBZRKA==",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.3.8",
|
||||
"@peculiar/asn1-csr": "^2.3.8",
|
||||
@ -8481,6 +8799,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bytestreamjs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
|
||||
"integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@ -12716,12 +13042,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -14120,6 +14446,22 @@
|
||||
"pathe": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pkijs": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.2.4.tgz",
|
||||
"integrity": "sha512-Et9V5QpvBilPFgagJcaKBqXjKrrgF5JL2mSDELk1vvbOTt4fuBhSSsGn9Tcz0TQTfS5GCpXQ31Whrpqeqp0VRg==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"bytestreamjs": "^2.0.0",
|
||||
"pvtsutils": "^1.3.2",
|
||||
"pvutils": "^1.1.3",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/plimit-lit": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz",
|
||||
@ -15155,6 +15497,34 @@
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
|
||||
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
|
||||
},
|
||||
"node_modules/scim-patch": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz",
|
||||
"integrity": "sha512-3d0wD4THAt03Zgi08kCQwc+lvPJ2v4wwk41b0xViVa4gLYSgRUCmGkJNBsaE+yoKg0fufTOJCcSrufZaqYn/og==",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"scim2-parse-filter": "0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/scim-patch/node_modules/@types/node": {
|
||||
"version": "22.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz",
|
||||
"integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/scim-patch/node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/scim2-parse-filter": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||
"integrity": "sha512-k5TgGSuQEbR4jXRgw/GPAYVL9fMp1pWA2abLF5z3q9IGWSuZTqbrZBOSUezvc+rtViXr+czSZjg3eAN4QSTvxQ=="
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||
@ -16268,9 +16638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
},
|
||||
"node_modules/tsup": {
|
||||
"version": "8.0.1",
|
||||
|
@ -50,6 +50,7 @@
|
||||
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||
@ -106,6 +107,7 @@
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-kms": "^3.609.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
@ -126,7 +128,7 @@
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@peculiar/x509": "^1.10.0",
|
||||
"@peculiar/x509": "^1.12.1",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@sindresorhus/slugify": "1.1.0",
|
||||
"@team-plain/typescript-sdk": "^4.6.1",
|
||||
@ -171,9 +173,12 @@
|
||||
"pg-query-stream": "^4.5.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"pkijs": "^3.2.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"scim-patch": "^0.8.3",
|
||||
"scim2-parse-filter": "^0.2.10",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -7,6 +7,7 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
@ -160,6 +161,7 @@ declare module "fastify" {
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@ -53,6 +53,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate,
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate,
|
||||
@ -372,6 +375,11 @@ declare module "knex/types/tables" {
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate
|
||||
>;
|
||||
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate
|
||||
>;
|
||||
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
||||
TCertificateBodies,
|
||||
TCertificateBodiesInsert,
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password");
|
||||
if (!doesPasswordExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.string("password").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password");
|
||||
if (doesPasswordExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("password");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateTemplateEstConfig);
|
||||
if (!hasEstConfigTable) {
|
||||
await knex.schema.createTable(TableName.CertificateTemplateEstConfig, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.uuid("certificateTemplateId").notNullable().unique();
|
||||
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
|
||||
tb.binary("encryptedCaChain").notNullable();
|
||||
tb.string("hashedPassphrase").notNullable();
|
||||
tb.boolean("isEnabled").notNullable();
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateTemplateEstConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
|
||||
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCrl, "caSecretId");
|
||||
if (!hasCaSecretIdColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
|
||||
t.uuid("caSecretId").nullable();
|
||||
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.CertificateAuthorityCrl}" crl
|
||||
SET "caSecretId" = (
|
||||
SELECT sec.id
|
||||
FROM "${TableName.CertificateAuthoritySecret}" sec
|
||||
WHERE sec."caId" = crl."caId"
|
||||
)
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
|
||||
t.uuid("caSecretId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
|
||||
t.dropColumn("caSecretId");
|
||||
});
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalRequestsReviewersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
member: z.string().uuid().nullable().optional(),
|
||||
status: z.string(),
|
||||
requestId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
|
@ -11,6 +11,7 @@ export const AccessApprovalRequestsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
privilegeId: z.string().uuid().nullable().optional(),
|
||||
requestedBy: z.string().uuid().nullable().optional(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
permissions: z.unknown(),
|
||||
|
@ -14,7 +14,8 @@ export const CertificateAuthorityCrlSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
caId: z.string().uuid(),
|
||||
encryptedCrl: zodBuffer
|
||||
encryptedCrl: zodBuffer,
|
||||
caSecretId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
|
||||
|
29
backend/src/db/schemas/certificate-template-est-configs.ts
Normal file
29
backend/src/db/schemas/certificate-template-est-configs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// 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 CertificateTemplateEstConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid(),
|
||||
encryptedCaChain: zodBuffer,
|
||||
hashedPassphrase: z.string(),
|
||||
isEnabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
||||
export type TCertificateTemplateEstConfigsInsert = Omit<
|
||||
z.input<typeof CertificateTemplateEstConfigsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TCertificateTemplateEstConfigsUpdate = Partial<
|
||||
Omit<z.input<typeof CertificateTemplateEstConfigsSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-template-est-configs";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
export enum TableName {
|
||||
Users = "users",
|
||||
CertificateAuthority = "certificate_authorities",
|
||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||
CertificateAuthorityCert = "certificate_authority_certs",
|
||||
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||
CertificateAuthorityCrl = "certificate_authority_crl",
|
||||
|
@ -10,6 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const ProjectUserAdditionalPrivilegeSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
projectMembershipId: z.string().uuid().nullable().optional(),
|
||||
isTemporary: z.boolean().default(false),
|
||||
temporaryMode: z.string().nullable().optional(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
|
@ -21,6 +21,7 @@ export const SecretSharingSchema = z.object({
|
||||
expiresAfterViews: z.number().nullable().optional(),
|
||||
accessType: z.string().default("anyone"),
|
||||
name: z.string().nullable().optional(),
|
||||
password: z.string().nullable().optional(),
|
||||
lastViewedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
|
173
backend/src/ee/routes/est/certificate-est-router.ts
Normal file
173
backend/src/ee/routes/est/certificate-est-router.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
|
||||
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
// add support for CSR bodies
|
||||
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
let csrBody = body as string;
|
||||
// some EST clients send CSRs in PEM format and some in base64 format
|
||||
// for CSRs sent in PEM, we leave them as is
|
||||
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
||||
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
||||
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
|
||||
}
|
||||
|
||||
done(null, csrBody);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
done(error, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
// Authenticate EST client using Passphrase
|
||||
server.addHook("onRequest", async (req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
const urlFragments = req.url.split("/");
|
||||
|
||||
// cacerts endpoint should not have any authentication
|
||||
if (urlFragments[urlFragments.length - 1] === "cacerts") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authorization) {
|
||||
const wwwAuthenticateHeader = "WWW-Authenticate";
|
||||
const errAuthRequired = "Authentication required";
|
||||
|
||||
await res.hijack();
|
||||
|
||||
// definitive connection timeout to clean-up open connections and prevent memory leak
|
||||
res.raw.setTimeout(10 * 1000, () => {
|
||||
res.raw.end();
|
||||
});
|
||||
|
||||
res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`);
|
||||
res.raw.setHeader("Content-Length", 0);
|
||||
res.raw.statusCode = 401;
|
||||
|
||||
// Write the error message to the response without ending the connection
|
||||
res.raw.write(errAuthRequired);
|
||||
|
||||
// flush headers
|
||||
res.raw.flushHeaders();
|
||||
return;
|
||||
}
|
||||
|
||||
const certificateTemplateId = urlFragments.slice(-2)[0];
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const rawCredential = authorization?.split(" ").pop();
|
||||
if (!rawCredential) {
|
||||
throw new UnauthorizedError({ message: "Missing HTTP credentials" });
|
||||
}
|
||||
|
||||
// expected format is user:password
|
||||
const basicCredential = atob(rawCredential);
|
||||
const password = basicCredential.split(":").pop();
|
||||
if (!password) {
|
||||
throw new BadRequestError({
|
||||
message: "No password provided"
|
||||
});
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, estConfig.hashedPassphrase);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simpleenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.simpleEnroll({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simplereenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.simpleReenroll({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/cacerts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.getCaCerts({
|
||||
certificateTemplateId: req.params.certificateTemplateId
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -1,86 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { CA_CRLS } 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";
|
||||
|
||||
export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/crl",
|
||||
url: "/:crlId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get CRL of the CA",
|
||||
description: "Get CRL in DER format",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
|
||||
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
|
||||
})
|
||||
200: z.instanceof(Buffer)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
handler: async (req, res) => {
|
||||
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA_CRL,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
res.header("Content-Type", "application/pkix-crl");
|
||||
|
||||
return {
|
||||
crl
|
||||
};
|
||||
return Buffer.from(crl);
|
||||
}
|
||||
});
|
||||
|
||||
// server.route({
|
||||
// method: "GET",
|
||||
// url: "/:caId/crl/rotate",
|
||||
// config: {
|
||||
// rateLimit: writeLimit
|
||||
// },
|
||||
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
// schema: {
|
||||
// description: "Rotate CRL of the CA",
|
||||
// params: z.object({
|
||||
// caId: z.string().trim()
|
||||
// }),
|
||||
// response: {
|
||||
// 200: z.object({
|
||||
// message: z.string()
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// handler: async (req) => {
|
||||
// await server.services.certificateAuthority.rotateCaCrl({
|
||||
// caId: req.params.caId,
|
||||
// actor: req.permission.type,
|
||||
// actorId: req.permission.id,
|
||||
// actorAuthMethod: req.permission.authMethod,
|
||||
// actorOrgId: req.permission.orgId
|
||||
// });
|
||||
// return {
|
||||
// message: "Successfully rotated CA CRL"
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
@ -61,7 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
|
||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||
},
|
||||
{ prefix: "/pki" }
|
||||
);
|
||||
|
@ -5,19 +5,47 @@ 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";
|
||||
|
||||
const ScimUserSchema = z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z
|
||||
.object({
|
||||
familyName: z.string().trim().optional(),
|
||||
givenName: z.string().trim().optional()
|
||||
})
|
||||
.optional(),
|
||||
emails: z
|
||||
.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
});
|
||||
|
||||
const ScimGroupSchema = z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string().optional()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
const strBody = body instanceof Buffer ? body.toString() : body;
|
||||
|
||||
const json: unknown = JSON.parse(strBody);
|
||||
done(null, json);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
done(error, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/scim-tokens",
|
||||
method: "POST",
|
||||
@ -124,25 +152,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
Resources: z.array(
|
||||
z.object({
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
}),
|
||||
emails: z.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string(),
|
||||
type: z.string().trim()
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
})
|
||||
),
|
||||
Resources: z.array(ScimUserSchema),
|
||||
itemsPerPage: z.number(),
|
||||
schemas: z.array(z.string()),
|
||||
startIndex: z.number(),
|
||||
@ -170,30 +180,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
orgMembershipId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
201: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
}),
|
||||
emails: z.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string(),
|
||||
type: z.string().trim()
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean(),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim()
|
||||
})
|
||||
)
|
||||
})
|
||||
200: ScimUserSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
@ -213,10 +200,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
}),
|
||||
name: z
|
||||
.object({
|
||||
familyName: z.string().trim().optional(),
|
||||
givenName: z.string().trim().optional()
|
||||
})
|
||||
.optional(),
|
||||
emails: z
|
||||
.array(
|
||||
z.object({
|
||||
@ -226,28 +215,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
// displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
active: z.boolean().default(true)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
}),
|
||||
emails: z.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
})
|
||||
200: ScimUserSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
@ -257,8 +228,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
const user = await req.server.services.scim.createScimUser({
|
||||
externalId: req.body.userName,
|
||||
email: primaryEmail,
|
||||
firstName: req.body.name.givenName,
|
||||
lastName: req.body.name.familyName,
|
||||
firstName: req.body?.name?.givenName,
|
||||
lastName: req.body?.name?.familyName,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
@ -288,6 +259,116 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Users/:orgMembershipId",
|
||||
method: "PUT",
|
||||
schema: {
|
||||
params: z.object({
|
||||
orgMembershipId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z
|
||||
.object({
|
||||
familyName: z.string().trim().optional(),
|
||||
givenName: z.string().trim().optional()
|
||||
})
|
||||
.optional(),
|
||||
displayName: z.string().trim(),
|
||||
emails: z
|
||||
.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
active: z.boolean()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
}),
|
||||
emails: z.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
|
||||
const user = await req.server.services.scim.replaceScimUser({
|
||||
orgMembershipId: req.params.orgMembershipId,
|
||||
orgId: req.permission.orgId,
|
||||
firstName: req.body?.name?.givenName,
|
||||
lastName: req.body?.name?.familyName,
|
||||
active: req.body?.active,
|
||||
email: primaryEmail,
|
||||
externalId: req.body.userName
|
||||
});
|
||||
return user;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Users/:orgMembershipId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
orgMembershipId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
Operations: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
op: z.union([z.literal("remove"), z.literal("Remove")]),
|
||||
path: z.string().trim(),
|
||||
value: z
|
||||
.object({
|
||||
value: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
z.object({
|
||||
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
|
||||
path: z.string().trim().optional(),
|
||||
value: z.any().optional()
|
||||
})
|
||||
])
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: ScimUserSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.updateScimUser({
|
||||
orgMembershipId: req.params.orgMembershipId,
|
||||
orgId: req.permission.orgId,
|
||||
operations: req.body.Operations
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
});
|
||||
server.route({
|
||||
url: "/Groups",
|
||||
method: "POST",
|
||||
@ -302,25 +383,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
display: z.string()
|
||||
})
|
||||
)
|
||||
.optional() // okta-specific
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
200: ScimGroupSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
@ -341,26 +407,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
querystring: z.object({
|
||||
startIndex: z.coerce.number().default(1),
|
||||
count: z.coerce.number().default(20),
|
||||
filter: z.string().trim().optional()
|
||||
filter: z.string().trim().optional(),
|
||||
excludedAttributes: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
Resources: z.array(
|
||||
z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
),
|
||||
Resources: z.array(ScimGroupSchema),
|
||||
itemsPerPage: z.number(),
|
||||
schemas: z.array(z.string()),
|
||||
startIndex: z.number(),
|
||||
@ -374,7 +426,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
orgId: req.permission.orgId,
|
||||
startIndex: req.query.startIndex,
|
||||
filter: req.query.filter,
|
||||
limit: req.query.count
|
||||
limit: req.query.count,
|
||||
isMembersExcluded: req.query.excludedAttributes === "members"
|
||||
});
|
||||
|
||||
return groups;
|
||||
@ -389,20 +442,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
groupId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
200: ScimGroupSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
@ -411,6 +451,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
@ -434,25 +475,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
200: ScimGroupSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const group = await req.server.services.scim.updateScimGroupNamePut({
|
||||
const group = await req.server.services.scim.replaceScimGroup({
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId,
|
||||
...req.body
|
||||
@ -474,54 +502,34 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
Operations: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
op: z.literal("replace"),
|
||||
value: z.object({
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim()
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("remove"),
|
||||
path: z.string().trim()
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("add"),
|
||||
op: z.union([z.literal("remove"), z.literal("Remove")]),
|
||||
path: z.string().trim(),
|
||||
value: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim().optional()
|
||||
value: z
|
||||
.object({
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
z.object({
|
||||
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
|
||||
path: z.string().trim().optional(),
|
||||
value: z.any()
|
||||
})
|
||||
])
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
200: ScimGroupSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const group = await req.server.services.scim.updateScimGroupNamePatch({
|
||||
const group = await req.server.services.scim.updateScimGroup({
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId,
|
||||
operations: req.body.Operations
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
@ -547,60 +555,4 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Users/:orgMembershipId",
|
||||
method: "PUT",
|
||||
schema: {
|
||||
params: z.object({
|
||||
orgMembershipId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
}),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
}),
|
||||
emails: z.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean(),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.replaceScimUser({
|
||||
orgMembershipId: req.params.orgMembershipId,
|
||||
orgId: req.permission.orgId,
|
||||
active: req.body.active
|
||||
});
|
||||
return user;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -137,7 +137,7 @@ export enum EventType {
|
||||
GET_CA_CERT = "get-certificate-authority-cert",
|
||||
SIGN_INTERMEDIATE = "sign-intermediate",
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRL = "get-certificate-authority-crl",
|
||||
GET_CA_CRLS = "get-certificate-authority-crls",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
SIGN_CERT = "sign-cert",
|
||||
GET_CERT = "get-cert",
|
||||
@ -166,7 +166,10 @@ export enum EventType {
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -1163,8 +1166,8 @@ interface ImportCaCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCrl {
|
||||
type: EventType.GET_CA_CRL;
|
||||
interface GetCaCrls {
|
||||
type: EventType.GET_CA_CRLS;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
@ -1420,6 +1423,29 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplateEstConfig {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplateEstConfig {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1518,7 +1544,7 @@ export type Event =
|
||||
| GetCaCert
|
||||
| SignIntermediate
|
||||
| ImportCaCert
|
||||
| GetCaCrl
|
||||
| GetCaCrls
|
||||
| IssueCert
|
||||
| SignCert
|
||||
| GetCert
|
||||
@ -1547,4 +1573,7 @@ export type Event =
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
| DeleteCertificateTemplate
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig;
|
||||
|
@ -2,24 +2,24 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
// import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { TGetCrl } from "./certificate-authority-crl-types";
|
||||
import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
|
||||
|
||||
type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
||||
@ -29,13 +29,42 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
certificateAuthorityCrlDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
licenseService
|
||||
permissionService // licenseService
|
||||
}: TCertificateAuthorityCrlServiceFactoryDep) => {
|
||||
/**
|
||||
* Return the Certificate Revocation List (CRL) for CA with id [caId]
|
||||
* Return CRL with id [crlId]
|
||||
*/
|
||||
const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => {
|
||||
const getCrlById = async (crlId: TGetCrlById) => {
|
||||
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
|
||||
if (!caCrl) throw new NotFoundError({ message: "CRL not found" });
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
|
||||
return {
|
||||
ca,
|
||||
caCrl,
|
||||
crl: crl.rawData
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of CRL ids for CA with id [caId]
|
||||
*/
|
||||
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
@ -52,15 +81,14 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.caCrl)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
|
||||
});
|
||||
// const plan = await licenseService.getPlan(actorOrgId);
|
||||
// if (!plan.caCrl)
|
||||
// throw new BadRequestError({
|
||||
// message:
|
||||
// "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
|
||||
// });
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id });
|
||||
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
|
||||
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -72,15 +100,23 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
const decryptedCrls = await Promise.all(
|
||||
caCrls.map(async (caCrl) => {
|
||||
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
|
||||
const base64crl = crl.toString("base64");
|
||||
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
||||
const base64crl = crl.toString("base64");
|
||||
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
||||
return {
|
||||
id: caCrl.id,
|
||||
crl: crlPem
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
crl: crlPem,
|
||||
ca
|
||||
ca,
|
||||
crls: decryptedCrls
|
||||
};
|
||||
};
|
||||
|
||||
@ -166,7 +202,8 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
// };
|
||||
|
||||
return {
|
||||
getCaCrl
|
||||
getCrlById,
|
||||
getCaCrls
|
||||
// rotateCaCrl
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetCrl = {
|
||||
export type TGetCrlById = string;
|
||||
|
||||
export type TGetCaCrlsDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
|
||||
|
||||
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
|
||||
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
|
||||
const cmsSigned = new SignedData({
|
||||
encapContentInfo: new EncapsulatedContentInfo({
|
||||
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
|
||||
}),
|
||||
certificates: certs
|
||||
});
|
||||
|
||||
const cmsContent = new ContentInfo({
|
||||
contentType: "1.2.840.113549.1.7.2", // SignedData
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
content: cmsSigned.toSchema()
|
||||
});
|
||||
|
||||
const derBuffer = cmsContent.toSchema().toBER(false);
|
||||
const base64Pkcs7 = Buffer.from(derBuffer)
|
||||
.toString("base64")
|
||||
.replace(/(.{64})/g, "$1\n"); // we add a linebreak for CURL clients
|
||||
|
||||
return base64Pkcs7;
|
||||
};
|
@ -0,0 +1,268 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
|
||||
|
||||
type TCertificateEstServiceFactoryDep = {
|
||||
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
|
||||
|
||||
export const certificateEstServiceFactory = ({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
licenseService
|
||||
}: TCertificateEstServiceFactoryDep) => {
|
||||
const simpleReenroll = async ({
|
||||
csr,
|
||||
certificateTemplateId,
|
||||
sslClientCert
|
||||
}: {
|
||||
csr: string;
|
||||
certificateTemplateId: string;
|
||||
sslClientCert: string;
|
||||
}) => {
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
const plan = await licenseService.getPlan(estConfig.orgId);
|
||||
if (!plan.pkiEst) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to perform EST operation - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const cert = new x509.X509Certificate(leafCertificate);
|
||||
// We have to assert that the client certificate provided can be traced back to the Root CA
|
||||
const caCertChains = await getCaCertChains({
|
||||
caId: certTemplate.caId,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const verifiedChains = await Promise.all(
|
||||
caCertChains.map((chain) => {
|
||||
const caCert = new x509.X509Certificate(chain.certificate);
|
||||
const caChain =
|
||||
chain.certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
|
||||
return isCertChainValid([cert, caCert, ...caChain]);
|
||||
})
|
||||
);
|
||||
|
||||
if (!verifiedChains.some(Boolean)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid client certificate: unable to build a valid certificate chain"
|
||||
});
|
||||
}
|
||||
|
||||
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
if (csrObj.subject !== cert.subject) {
|
||||
throw new BadRequestError({
|
||||
message: "Subject mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
let csrSanSet: Set<string> = new Set();
|
||||
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (csrSanExtension) {
|
||||
const sanNames = new x509.GeneralNames(csrSanExtension.value);
|
||||
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
|
||||
}
|
||||
|
||||
let certSanSet: Set<string> = new Set();
|
||||
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (certSanExtension) {
|
||||
const sanNames = new x509.GeneralNames(certSanExtension.value);
|
||||
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
|
||||
}
|
||||
|
||||
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
|
||||
throw new BadRequestError({
|
||||
message: "Subject alternative names mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
});
|
||||
|
||||
return convertRawCertsToPkcs7([certificate.rawData]);
|
||||
};
|
||||
|
||||
const simpleEnroll = async ({
|
||||
csr,
|
||||
certificateTemplateId,
|
||||
sslClientCert
|
||||
}: {
|
||||
csr: string;
|
||||
certificateTemplateId: string;
|
||||
sslClientCert: string;
|
||||
}) => {
|
||||
/* We first have to assert that the client certificate provided can be traced back to the attached
|
||||
CA chain in the EST configuration
|
||||
*/
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
const plan = await licenseService.getPlan(estConfig.orgId);
|
||||
if (!plan.pkiEst) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to perform EST operation - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const certObj = new x509.X509Certificate(leafCertificate);
|
||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
});
|
||||
|
||||
return convertRawCertsToPkcs7([certificate.rawData]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the CA certificate and CA certificate chain for the CA bound to
|
||||
* the certificate template with id [certificateTemplateId] as part of EST protocol
|
||||
*/
|
||||
const getCaCerts = async ({ certificateTemplateId }: { certificateTemplateId: string }) => {
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
const plan = await licenseService.getPlan(estConfig.orgId);
|
||||
if (!plan.pkiEst) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to perform EST operation - caCerts due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
|
||||
if (!ca) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate Authority not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId as string,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificates = caCertChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const caCertificate = new x509.X509Certificate(caCert);
|
||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||
};
|
||||
|
||||
return {
|
||||
simpleEnroll,
|
||||
simpleReenroll,
|
||||
getCaCerts
|
||||
};
|
||||
};
|
@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
|
@ -0,0 +1,226 @@
|
||||
import {
|
||||
CreateUserCommand,
|
||||
CreateUserGroupCommand,
|
||||
DeleteUserCommand,
|
||||
DescribeReplicationGroupsCommand,
|
||||
DescribeUserGroupsCommand,
|
||||
ElastiCache,
|
||||
ModifyReplicationGroupCommand,
|
||||
ModifyUserGroupCommand
|
||||
} from "@aws-sdk/client-elasticache";
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const CreateElastiCacheUserSchema = z.object({
|
||||
UserId: z.string().trim().min(1),
|
||||
UserName: z.string().trim().min(1),
|
||||
Engine: z.string().default("redis"),
|
||||
Passwords: z.array(z.string().trim().min(1)).min(1).max(1), // Minimum password length is 16 characters, required by AWS.
|
||||
AccessString: z.string().trim().min(1) // Example: "on ~* +@all"
|
||||
});
|
||||
|
||||
const DeleteElasticCacheUserSchema = z.object({
|
||||
UserId: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
type TElastiCacheRedisUser = { userId: string; password: string };
|
||||
type TBasicAWSCredentials = { accessKeyId: string; secretAccessKey: string };
|
||||
|
||||
type TCreateElastiCacheUserInput = z.infer<typeof CreateElastiCacheUserSchema>;
|
||||
type TDeleteElastiCacheUserInput = z.infer<typeof DeleteElasticCacheUserSchema>;
|
||||
|
||||
const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => {
|
||||
const elastiCache = new ElastiCache({
|
||||
region,
|
||||
credentials
|
||||
});
|
||||
const infisicalGroup = "infisical-managed-group-elasticache";
|
||||
|
||||
const ensureInfisicalGroupExists = async (clusterName: string) => {
|
||||
const replicationGroups = await elastiCache.send(new DescribeUserGroupsCommand());
|
||||
|
||||
const existingGroup = replicationGroups.UserGroups?.find((group) => group.UserGroupId === infisicalGroup);
|
||||
|
||||
let newlyCreatedGroup = false;
|
||||
if (!existingGroup) {
|
||||
const createGroupCommand = new CreateUserGroupCommand({
|
||||
UserGroupId: infisicalGroup,
|
||||
UserIds: ["default"],
|
||||
Engine: "redis"
|
||||
});
|
||||
|
||||
await elastiCache.send(createGroupCommand);
|
||||
newlyCreatedGroup = true;
|
||||
}
|
||||
|
||||
if (existingGroup || newlyCreatedGroup) {
|
||||
const replicationGroup = (
|
||||
await elastiCache.send(
|
||||
new DescribeReplicationGroupsCommand({
|
||||
ReplicationGroupId: clusterName
|
||||
})
|
||||
)
|
||||
).ReplicationGroups?.[0];
|
||||
|
||||
if (!replicationGroup?.UserGroupIds?.includes(infisicalGroup)) {
|
||||
// If the replication group doesn't have the infisical user group, we need to associate it
|
||||
const modifyGroupCommand = new ModifyReplicationGroupCommand({
|
||||
UserGroupIdsToAdd: [infisicalGroup],
|
||||
UserGroupIdsToRemove: [],
|
||||
ApplyImmediately: true,
|
||||
ReplicationGroupId: clusterName
|
||||
});
|
||||
await elastiCache.send(modifyGroupCommand);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addUserToInfisicalGroup = async (userId: string) => {
|
||||
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
|
||||
|
||||
const addUserToGroupCommand = new ModifyUserGroupCommand({
|
||||
UserGroupId: infisicalGroup,
|
||||
UserIdsToAdd: [userId],
|
||||
UserIdsToRemove: []
|
||||
});
|
||||
|
||||
await elastiCache.send(addUserToGroupCommand);
|
||||
};
|
||||
|
||||
const createUser = async (creationInput: TCreateElastiCacheUserInput, clusterName: string) => {
|
||||
await ensureInfisicalGroupExists(clusterName);
|
||||
|
||||
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
|
||||
await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
|
||||
|
||||
return {
|
||||
userId: creationInput.UserId,
|
||||
password: creationInput.Passwords[0]
|
||||
};
|
||||
};
|
||||
|
||||
const deleteUser = async (
|
||||
deletionInput: TDeleteElastiCacheUserInput
|
||||
): Promise<Pick<TElastiCacheRedisUser, "userId">> => {
|
||||
await elastiCache.send(new DeleteUserCommand(deletionInput));
|
||||
return { userId: deletionInput.UserId };
|
||||
};
|
||||
|
||||
const verifyCredentials = async (clusterName: string) => {
|
||||
await elastiCache.send(
|
||||
new DescribeReplicationGroupsCommand({
|
||||
ReplicationGroupId: clusterName
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
createUser,
|
||||
deleteUser,
|
||||
verifyCredentials
|
||||
};
|
||||
};
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
|
||||
return `inf-${customAlphabet(charset, 32)()}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
};
|
||||
|
||||
export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = DynamicSecretAwsElastiCacheSchema.parse(inputs);
|
||||
|
||||
// We need to ensure the that the creation & revocation statements are valid and can be used to create and revoke users.
|
||||
// We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements.
|
||||
CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement));
|
||||
DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement));
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).verifyCredentials(providerInputs.clusterName);
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!(await validateConnection(providerInputs))) {
|
||||
throw new BadRequestError({ message: "Failed to establish connection" });
|
||||
}
|
||||
|
||||
const leaseUsername = generateUsername();
|
||||
const leasePassword = generatePassword();
|
||||
const leaseExpiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username: leaseUsername,
|
||||
password: leasePassword,
|
||||
expiration: leaseExpiration
|
||||
});
|
||||
|
||||
const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement));
|
||||
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).createUser(parsedStatement, providerInputs.clusterName);
|
||||
|
||||
return {
|
||||
entityId: leaseUsername,
|
||||
data: {
|
||||
DB_USERNAME: leaseUsername,
|
||||
DB_PASSWORD: leasePassword
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId });
|
||||
const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement));
|
||||
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).deleteUser(parsedStatement);
|
||||
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
// Do nothing
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -1,10 +1,16 @@
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||
import { RedisDatabaseProvider } from "./redis";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
|
||||
export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider()
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
|
||||
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider()
|
||||
});
|
||||
|
@ -7,6 +7,29 @@ export enum SqlProviders {
|
||||
MsSQL = "mssql"
|
||||
}
|
||||
|
||||
export const DynamicSecretRedisDBSchema = z.object({
|
||||
host: z.string().trim().toLowerCase(),
|
||||
port: z.number(),
|
||||
username: z.string().trim(), // this is often "default".
|
||||
password: z.string().trim().optional(),
|
||||
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretAwsElastiCacheSchema = z.object({
|
||||
clusterName: z.string().trim().min(1),
|
||||
accessKeyId: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
|
||||
region: z.string().trim(),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretSqlDBSchema = z.object({
|
||||
client: z.nativeEnum(SqlProviders),
|
||||
host: z.string().trim().toLowerCase(),
|
||||
@ -44,16 +67,59 @@ export const DynamicSecretAwsIamSchema = z.object({
|
||||
policyArns: z.string().trim().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretMongoAtlasSchema = z.object({
|
||||
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
|
||||
adminPrivateKey: z.string().trim().min(1).describe("Admin user private api key"),
|
||||
groupId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.describe("Unique 24-hexadecimal digit string that identifies your project. This is same as project id"),
|
||||
roles: z
|
||||
.object({
|
||||
collectionName: z.string().optional().describe("Collection on which this role applies."),
|
||||
databaseName: z.string().min(1).describe("Database to which the user is granted access privileges."),
|
||||
roleName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe(
|
||||
' Enum: "atlasAdmin" "backup" "clusterMonitor" "dbAdmin" "dbAdminAnyDatabase" "enableSharding" "read" "readAnyDatabase" "readWrite" "readWriteAnyDatabase" "<a custom role name>".Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.'
|
||||
)
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
scopes: z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe(
|
||||
"Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access."
|
||||
),
|
||||
type: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("Category of resource that this database user can access. Enum: CLUSTER, DATA_LAKE, STREAM")
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
AwsIam = "aws-iam"
|
||||
AwsIam = "aws-iam",
|
||||
Redis = "redis",
|
||||
AwsElastiCache = "aws-elasticache",
|
||||
MongoAtlas = "mongo-db-atlas"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
146
backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts
Normal file
146
backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretMongoAtlasSchema.parseAsync(inputs);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
|
||||
const client = axios.create({
|
||||
baseURL: "https://cloud.mongodb.com/api/atlas",
|
||||
headers: {
|
||||
Accept: "application/vnd.atlas.2023-02-01+json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
const digestAuth = createDigestAuthRequestInterceptor(
|
||||
client,
|
||||
providerInputs.adminPublicKey,
|
||||
providerInputs.adminPrivateKey
|
||||
);
|
||||
return digestAuth;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const isConnected = await client({
|
||||
method: "GET",
|
||||
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
params: { itemsPerPage: 1 }
|
||||
})
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
await client({
|
||||
method: "POST",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
data: {
|
||||
roles: providerInputs.roles,
|
||||
scopes: providerInputs.scopes,
|
||||
deleteAfterDate: expiration,
|
||||
username,
|
||||
password,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const isExisting = await client({
|
||||
method: "GET",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
|
||||
}).catch((err) => {
|
||||
if ((err as AxiosError).response?.status === 404) return false;
|
||||
throw err;
|
||||
});
|
||||
if (isExisting) {
|
||||
await client({
|
||||
method: "DELETE",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
await client({
|
||||
method: "PATCH",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
|
||||
data: {
|
||||
deleteAfterDate: expiration,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
183
backend/src/ee/services/dynamic-secret/providers/redis.ts
Normal file
183
backend/src/ee/services/dynamic-secret/providers/redis.ts
Normal file
@ -0,0 +1,183 @@
|
||||
/* eslint-disable no-console */
|
||||
import handlebars from "handlebars";
|
||||
import { Redis } from "ioredis";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
const executeTransactions = async (connection: Redis, commands: string[]): Promise<(string | null)[] | null> => {
|
||||
// Initiate a transaction
|
||||
const pipeline = connection.multi();
|
||||
|
||||
// Add all commands to the pipeline
|
||||
for (const command of commands) {
|
||||
const args = command
|
||||
.split(" ")
|
||||
.map((arg) => arg.trim())
|
||||
.filter((arg) => arg.length > 0);
|
||||
pipeline.call(args[0], ...args.slice(1));
|
||||
}
|
||||
|
||||
// Execute the transaction
|
||||
const results = await pipeline.exec();
|
||||
|
||||
if (!results) {
|
||||
throw new BadRequestError({ message: "Redis transaction failed: No results returned" });
|
||||
}
|
||||
|
||||
// Check for errors in the results
|
||||
const errors = results.filter(([err]) => err !== null);
|
||||
if (errors.length > 0) {
|
||||
throw new BadRequestError({ message: "Redis transaction failed with errors" });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return results.map(([_, result]) => result as string | null);
|
||||
};
|
||||
|
||||
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
|
||||
if (
|
||||
isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||
let connection: Redis | null = null;
|
||||
try {
|
||||
connection = new Redis({
|
||||
username: providerInputs.username,
|
||||
host: providerInputs.host,
|
||||
port: providerInputs.port,
|
||||
password: providerInputs.password,
|
||||
...(providerInputs.ca && {
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
ca: providerInputs.ca
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let result: string;
|
||||
if (providerInputs.password) {
|
||||
result = await connection.auth(providerInputs.username, providerInputs.password, () => {});
|
||||
} else {
|
||||
result = await connection.auth(providerInputs.username, () => {});
|
||||
}
|
||||
|
||||
if (result !== "OK") {
|
||||
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
|
||||
}
|
||||
|
||||
return connection;
|
||||
} catch (err) {
|
||||
if (connection) await connection.quit();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
|
||||
const pingResponse = await connection
|
||||
.ping()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
return pingResponse;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
|
||||
await executeTransactions(connection, queries);
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
|
||||
await executeTransactions(connection, queries);
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
||||
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await executeTransactions(connection, queries);
|
||||
}
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -45,7 +45,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
readLimit: 60,
|
||||
writeLimit: 200,
|
||||
secretsLimit: 40
|
||||
}
|
||||
},
|
||||
pkiEst: false
|
||||
});
|
||||
|
||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -63,6 +63,7 @@ export type TFeatureSet = {
|
||||
writeLimit: number;
|
||||
secretsLimit: number;
|
||||
};
|
||||
pkiEst: boolean;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -44,19 +44,18 @@ export const buildScimUser = ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
groups = [],
|
||||
active
|
||||
active,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}: {
|
||||
orgMembershipId: string;
|
||||
username: string;
|
||||
email?: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
groups?: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
firstName: string | null | undefined;
|
||||
lastName: string | null | undefined;
|
||||
active: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): TScimUser => {
|
||||
const scimUser = {
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
@ -64,9 +63,9 @@ export const buildScimUser = ({
|
||||
userName: username,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
name: {
|
||||
givenName: firstName,
|
||||
givenName: firstName || "",
|
||||
middleName: null,
|
||||
familyName: lastName
|
||||
familyName: lastName || ""
|
||||
},
|
||||
emails: email
|
||||
? [
|
||||
@ -78,10 +77,10 @@ export const buildScimUser = ({
|
||||
]
|
||||
: [],
|
||||
active,
|
||||
groups,
|
||||
meta: {
|
||||
resourceType: "User",
|
||||
location: null
|
||||
created: createdAt,
|
||||
lastModified: updatedAt
|
||||
}
|
||||
};
|
||||
|
||||
@ -109,14 +108,18 @@ export const buildScimGroupList = ({
|
||||
export const buildScimGroup = ({
|
||||
groupId,
|
||||
name,
|
||||
members
|
||||
members,
|
||||
updatedAt,
|
||||
createdAt
|
||||
}: {
|
||||
groupId: string;
|
||||
name: string;
|
||||
members: {
|
||||
value: string;
|
||||
display: string;
|
||||
display?: string;
|
||||
}[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): TScimGroup => {
|
||||
const scimGroup = {
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
@ -125,7 +128,8 @@ export const buildScimGroup = ({
|
||||
members,
|
||||
meta: {
|
||||
resourceType: "Group",
|
||||
location: null
|
||||
created: createdAt,
|
||||
lastModified: updatedAt
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { scimPatch } from "scim-patch";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
@ -9,7 +10,6 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
|
||||
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
@ -32,14 +32,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import {
|
||||
buildScimGroup,
|
||||
buildScimGroupList,
|
||||
buildScimUser,
|
||||
buildScimUserList,
|
||||
extractScimValueFromPath,
|
||||
parseScimFilter
|
||||
} from "./scim-fns";
|
||||
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
|
||||
import {
|
||||
TCreateScimGroupDTO,
|
||||
TCreateScimTokenDTO,
|
||||
@ -64,12 +57,18 @@ type TScimServiceFactoryDep = {
|
||||
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
|
||||
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete" | "update">;
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
|
||||
| "createMembership"
|
||||
| "findById"
|
||||
| "findMembership"
|
||||
| "findMembershipWithScimFilter"
|
||||
| "deleteMembershipById"
|
||||
| "transaction"
|
||||
| "updateMembershipById"
|
||||
>;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
||||
@ -193,7 +192,12 @@ export const scimServiceFactory = ({
|
||||
};
|
||||
|
||||
// SCIM server endpoints
|
||||
const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
|
||||
const listScimUsers = async ({
|
||||
startIndex = 0,
|
||||
limit = 100,
|
||||
filter,
|
||||
orgId
|
||||
}: TListScimUsersDTO): Promise<TListScimUsers> => {
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org.scimEnabled)
|
||||
@ -207,23 +211,20 @@ export const scimServiceFactory = ({
|
||||
...(limit && { limit })
|
||||
};
|
||||
|
||||
const users = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
|
||||
...parseScimFilter(filter)
|
||||
},
|
||||
findOpts
|
||||
);
|
||||
const users = await orgDAL.findMembershipWithScimFilter(orgId, filter, findOpts);
|
||||
|
||||
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
|
||||
buildScimUser({
|
||||
orgMembershipId: id ?? "",
|
||||
username: externalId ?? username,
|
||||
firstName: firstName ?? "",
|
||||
lastName: lastName ?? "",
|
||||
email,
|
||||
active: isActive
|
||||
})
|
||||
const scimUsers = users.map(
|
||||
({ id, externalId, username, firstName, lastName, email, isActive, createdAt, updatedAt }) =>
|
||||
buildScimUser({
|
||||
orgMembershipId: id ?? "",
|
||||
username: externalId ?? username,
|
||||
firstName: firstName ?? "",
|
||||
lastName: lastName ?? "",
|
||||
email,
|
||||
active: isActive,
|
||||
createdAt,
|
||||
updatedAt
|
||||
})
|
||||
);
|
||||
|
||||
return buildScimUserList({
|
||||
@ -258,22 +259,15 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
|
||||
membership.userId,
|
||||
orgId
|
||||
);
|
||||
|
||||
return buildScimUser({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email ?? "",
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active: membership.isActive,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
display: group.groupName
|
||||
}))
|
||||
createdAt: membership.createdAt,
|
||||
updatedAt: membership.updatedAt
|
||||
});
|
||||
};
|
||||
|
||||
@ -322,7 +316,7 @@ export const scimServiceFactory = ({
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
role: OrgMembershipRole.NoAccess,
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
@ -349,7 +343,11 @@ export const scimServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
|
||||
const uniqueUsername = await normalizeUsername(
|
||||
// external id is username
|
||||
`${firstName}-${lastName}`,
|
||||
userDAL
|
||||
);
|
||||
user = await userDAL.create(
|
||||
{
|
||||
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
|
||||
@ -427,13 +425,16 @@ export const scimServiceFactory = ({
|
||||
return buildScimUser({
|
||||
orgMembershipId: createdOrgMembership.id,
|
||||
username: externalId,
|
||||
firstName: createdUser.firstName as string,
|
||||
lastName: createdUser.lastName as string,
|
||||
firstName: createdUser.firstName,
|
||||
lastName: createdUser.lastName,
|
||||
email: createdUser.email ?? "",
|
||||
active: createdOrgMembership.isActive
|
||||
active: createdOrgMembership.isActive,
|
||||
createdAt: createdOrgMembership.createdAt,
|
||||
updatedAt: createdOrgMembership.updatedAt
|
||||
});
|
||||
};
|
||||
|
||||
// partial
|
||||
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
@ -459,37 +460,52 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
let active = true;
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (operation.op.toLowerCase() === "replace") {
|
||||
if (operation.path === "active" && operation.value === "False") {
|
||||
// azure scim op format
|
||||
active = false;
|
||||
} else if (typeof operation.value === "object" && operation.value.active === false) {
|
||||
// okta scim op format
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!active) {
|
||||
await orgMembershipDAL.updateById(membership.id, {
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
|
||||
return buildScimUser({
|
||||
const scimUser = buildScimUser({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
active
|
||||
lastName: membership.lastName,
|
||||
firstName: membership.firstName,
|
||||
active: membership.isActive,
|
||||
username: membership.externalId ?? membership.username,
|
||||
createdAt: membership.createdAt,
|
||||
updatedAt: membership.updatedAt
|
||||
});
|
||||
scimPatch(scimUser, operations);
|
||||
|
||||
const serverCfg = await getServerCfg();
|
||||
await userDAL.transaction(async (tx) => {
|
||||
await orgMembershipDAL.updateById(
|
||||
membership.id,
|
||||
{
|
||||
isActive: scimUser.active
|
||||
},
|
||||
tx
|
||||
);
|
||||
const hasEmailChanged = scimUser.emails[0].value !== membership.email;
|
||||
await userDAL.updateById(
|
||||
membership.userId,
|
||||
{
|
||||
firstName: scimUser.name.givenName,
|
||||
email: scimUser.emails[0].value,
|
||||
lastName: scimUser.name.familyName,
|
||||
isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return scimUser;
|
||||
};
|
||||
|
||||
const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
|
||||
const replaceScimUser = async ({
|
||||
orgMembershipId,
|
||||
active,
|
||||
orgId,
|
||||
lastName,
|
||||
firstName,
|
||||
email,
|
||||
externalId
|
||||
}: TReplaceScimUserDTO) => {
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||
@ -514,26 +530,47 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
await orgMembershipDAL.updateById(membership.id, {
|
||||
isActive: active
|
||||
const serverCfg = await getServerCfg();
|
||||
await userDAL.transaction(async (tx) => {
|
||||
await userAliasDAL.update(
|
||||
{
|
||||
orgId,
|
||||
aliasType: UserAliasType.SAML,
|
||||
userId: membership.userId
|
||||
},
|
||||
{
|
||||
externalId
|
||||
},
|
||||
tx
|
||||
);
|
||||
await orgMembershipDAL.updateById(
|
||||
membership.id,
|
||||
{
|
||||
isActive: active
|
||||
},
|
||||
tx
|
||||
);
|
||||
await userDAL.updateById(
|
||||
membership.userId,
|
||||
{
|
||||
firstName,
|
||||
email,
|
||||
lastName,
|
||||
isEmailVerified: serverCfg.trustSamlEmails
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
|
||||
membership.userId,
|
||||
orgId
|
||||
);
|
||||
|
||||
return buildScimUser({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
username: externalId,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
display: group.groupName
|
||||
}))
|
||||
createdAt: membership.createdAt,
|
||||
updatedAt: membership.updatedAt
|
||||
});
|
||||
};
|
||||
|
||||
@ -570,7 +607,7 @@ export const scimServiceFactory = ({
|
||||
return {}; // intentionally return empty object upon success
|
||||
};
|
||||
|
||||
const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
|
||||
const listScimGroups = async ({ orgId, startIndex, limit, filter, isMembersExcluded }: TListScimGroupsDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
@ -603,6 +640,21 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
|
||||
const scimGroups: TScimGroup[] = [];
|
||||
if (isMembersExcluded) {
|
||||
return buildScimGroupList({
|
||||
scimGroups: groups.map((group) =>
|
||||
buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: [],
|
||||
createdAt: group.createdAt,
|
||||
updatedAt: group.updatedAt
|
||||
})
|
||||
),
|
||||
startIndex,
|
||||
limit
|
||||
});
|
||||
}
|
||||
|
||||
for await (const group of groups) {
|
||||
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
|
||||
@ -612,7 +664,9 @@ export const scimServiceFactory = ({
|
||||
members: members.map((member) => ({
|
||||
value: member.orgMembershipId,
|
||||
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
|
||||
}))
|
||||
})),
|
||||
createdAt: group.createdAt,
|
||||
updatedAt: group.updatedAt
|
||||
});
|
||||
scimGroups.push(scimGroup);
|
||||
}
|
||||
@ -696,7 +750,9 @@ export const scimServiceFactory = ({
|
||||
members: orgMemberships.map(({ id, firstName, lastName }) => ({
|
||||
value: id,
|
||||
display: `${firstName} ${lastName}`
|
||||
}))
|
||||
})),
|
||||
createdAt: newGroup.group.createdAt,
|
||||
updatedAt: newGroup.group.updatedAt
|
||||
});
|
||||
};
|
||||
|
||||
@ -739,31 +795,17 @@ export const scimServiceFactory = ({
|
||||
members: orgMemberships.map(({ id, firstName, lastName }) => ({
|
||||
value: id,
|
||||
display: `${firstName} ${lastName}`
|
||||
}))
|
||||
})),
|
||||
createdAt: group.createdAt,
|
||||
updatedAt: group.updatedAt
|
||||
});
|
||||
};
|
||||
|
||||
const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
|
||||
});
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
if (!org) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Organization Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
|
||||
const $replaceGroupDAL = async (
|
||||
groupId: string,
|
||||
orgId: string,
|
||||
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
|
||||
) => {
|
||||
const updatedGroup = await groupDAL.transaction(async (tx) => {
|
||||
const [group] = await groupDAL.update(
|
||||
{
|
||||
@ -782,74 +824,96 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (members) {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: members.map((member) => member.value)
|
||||
}
|
||||
const orgMemberships = members.length
|
||||
? await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: members.map((member) => member.value)
|
||||
}
|
||||
})
|
||||
: [];
|
||||
|
||||
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
|
||||
const userGroupMembers = await userGroupMembershipDAL.find({
|
||||
groupId: group.id
|
||||
});
|
||||
const directMemberUserIds = userGroupMembers.filter((el) => !el.isPending).map((membership) => membership.userId);
|
||||
|
||||
const pendingGroupAdditionsUserIds = userGroupMembers
|
||||
.filter((el) => el.isPending)
|
||||
.map((pendingGroupAddition) => pendingGroupAddition.userId);
|
||||
|
||||
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
|
||||
const allMembersUserIdsSet = new Set(allMembersUserIds);
|
||||
|
||||
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
|
||||
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
|
||||
|
||||
if (toAddUserIds.length) {
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: toAddUserIds.map((member) => member.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
||||
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
|
||||
|
||||
const directMemberUserIds = (
|
||||
await userGroupMembershipDAL.find({
|
||||
groupId: group.id,
|
||||
isPending: false
|
||||
})
|
||||
).map((membership) => membership.userId);
|
||||
|
||||
const pendingGroupAdditionsUserIds = (
|
||||
await userGroupMembershipDAL.find({
|
||||
groupId: group.id,
|
||||
isPending: true
|
||||
})
|
||||
).map((pendingGroupAddition) => pendingGroupAddition.userId);
|
||||
|
||||
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
|
||||
const allMembersUserIdsSet = new Set(allMembersUserIds);
|
||||
|
||||
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
|
||||
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
|
||||
|
||||
if (toAddUserIds.length) {
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: toAddUserIds.map((member) => member.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
||||
if (toRemoveUserIds.length) {
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: toRemoveUserIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
if (toRemoveUserIds.length) {
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: toRemoveUserIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
||||
return group;
|
||||
});
|
||||
|
||||
return updatedGroup;
|
||||
};
|
||||
|
||||
const replaceScimGroup = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
|
||||
});
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
if (!org) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Organization Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
|
||||
const updatedGroup = await $replaceGroupDAL(groupId, orgId, { displayName, members });
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: updatedGroup.id,
|
||||
name: updatedGroup.name,
|
||||
members
|
||||
members,
|
||||
updatedAt: updatedGroup.updatedAt,
|
||||
createdAt: updatedGroup.createdAt
|
||||
});
|
||||
};
|
||||
|
||||
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
|
||||
const updateScimGroup = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
@ -871,7 +935,7 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
let group = await groupDAL.findOne({
|
||||
const group = await groupDAL.findOne({
|
||||
id: groupId,
|
||||
orgId
|
||||
});
|
||||
@ -883,73 +947,28 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
for await (const operation of operations) {
|
||||
switch (operation.op) {
|
||||
case "replace": {
|
||||
group = await groupDAL.updateById(group.id, {
|
||||
name: operation.value.displayName
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "add": {
|
||||
try {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: operation.value.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
} catch {
|
||||
logger.info("Repeat SCIM user-group add operation");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
const orgMembershipId = extractScimValueFromPath(operation.path);
|
||||
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
|
||||
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
|
||||
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [orgMembership.userId as string],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new ScimRequestError({
|
||||
detail: "Invalid Operation",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
|
||||
|
||||
return buildScimGroup({
|
||||
const scimGroup = buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: members.map((member) => ({
|
||||
value: member.orgMembershipId
|
||||
})),
|
||||
createdAt: group.createdAt,
|
||||
updatedAt: group.updatedAt
|
||||
});
|
||||
scimPatch(scimGroup, operations);
|
||||
// remove members is a weird case not following scim convention
|
||||
await $replaceGroupDAL(groupId, orgId, { displayName: scimGroup.displayName, members: scimGroup.members });
|
||||
|
||||
const updatedScimMembers = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
|
||||
return {
|
||||
...scimGroup,
|
||||
members: updatedScimMembers.map((member) => ({
|
||||
value: member.orgMembershipId,
|
||||
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
|
||||
}))
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
|
||||
@ -1025,8 +1044,8 @@ export const scimServiceFactory = ({
|
||||
createScimGroup,
|
||||
getScimGroup,
|
||||
deleteScimGroup,
|
||||
updateScimGroupNamePut,
|
||||
updateScimGroupNamePatch,
|
||||
replaceScimGroup,
|
||||
updateScimGroup,
|
||||
fnValidateScimToken
|
||||
};
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ScimPatchOperation } from "scim-patch";
|
||||
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateScimTokenDTO = {
|
||||
@ -34,29 +36,25 @@ export type TGetScimUserDTO = {
|
||||
export type TCreateScimUserDTO = {
|
||||
externalId: string;
|
||||
email?: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TUpdateScimUserDTO = {
|
||||
orgMembershipId: string;
|
||||
orgId: string;
|
||||
operations: {
|
||||
op: string;
|
||||
path?: string;
|
||||
value?:
|
||||
| string
|
||||
| {
|
||||
active: boolean;
|
||||
};
|
||||
}[];
|
||||
operations: ScimPatchOperation[];
|
||||
};
|
||||
|
||||
export type TReplaceScimUserDTO = {
|
||||
orgMembershipId: string;
|
||||
active: boolean;
|
||||
orgId: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
externalId: string;
|
||||
};
|
||||
|
||||
export type TDeleteScimUserDTO = {
|
||||
@ -69,6 +67,7 @@ export type TListScimGroupsDTO = {
|
||||
filter?: string;
|
||||
limit: number;
|
||||
orgId: string;
|
||||
isMembersExcluded?: boolean;
|
||||
};
|
||||
|
||||
export type TListScimGroups = {
|
||||
@ -107,29 +106,7 @@ export type TUpdateScimGroupNamePutDTO = {
|
||||
export type TUpdateScimGroupNamePatchDTO = {
|
||||
groupId: string;
|
||||
orgId: string;
|
||||
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
|
||||
};
|
||||
|
||||
type TReplaceOp = {
|
||||
op: "replace";
|
||||
value: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TRemoveOp = {
|
||||
op: "remove";
|
||||
path: string;
|
||||
};
|
||||
|
||||
type TAddOp = {
|
||||
op: "add";
|
||||
path: string;
|
||||
value: {
|
||||
value: string;
|
||||
display?: string;
|
||||
}[];
|
||||
operations: ScimPatchOperation[];
|
||||
};
|
||||
|
||||
export type TDeleteScimGroupDTO = {
|
||||
@ -158,13 +135,10 @@ export type TScimUser = {
|
||||
type: string;
|
||||
}[];
|
||||
active: boolean;
|
||||
groups: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
meta: {
|
||||
resourceType: string;
|
||||
location: null;
|
||||
created: Date;
|
||||
lastModified: Date;
|
||||
};
|
||||
};
|
||||
|
||||
@ -174,10 +148,11 @@ export type TScimGroup = {
|
||||
displayName: string;
|
||||
members: {
|
||||
value: string;
|
||||
display: string;
|
||||
display?: string;
|
||||
}[];
|
||||
meta: {
|
||||
resourceType: string;
|
||||
location: null;
|
||||
created: Date;
|
||||
lastModified: Date;
|
||||
};
|
||||
};
|
||||
|
@ -20,7 +20,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
|
||||
|
||||
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
|
||||
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
|
||||
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
|
||||
)
|
||||
.select(
|
||||
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
@ -47,8 +55,11 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId }) => ({
|
||||
userId: approverUserId
|
||||
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
|
||||
userId: approverUserId,
|
||||
email: approverEmail,
|
||||
firstName: approverFirstName,
|
||||
lastName: approverLastName
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { TSecretApprovalRequests } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||
|
||||
type TSendApprovalEmails = {
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectWithOrg">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectId: string;
|
||||
secretApprovalRequest: TSecretApprovalRequests;
|
||||
};
|
||||
|
||||
export const sendApprovalEmailsFn = async ({
|
||||
secretApprovalPolicyDAL,
|
||||
projectDAL,
|
||||
smtpService,
|
||||
projectId,
|
||||
secretApprovalRequest
|
||||
}: TSendApprovalEmails) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
const policy = await secretApprovalPolicyDAL.findById(secretApprovalRequest.policyId);
|
||||
|
||||
const project = await projectDAL.findProjectWithOrg(projectId);
|
||||
|
||||
// now we need to go through each of the reviewers and print out all the commits that they need to approve
|
||||
for await (const reviewerUser of policy.userApprovers) {
|
||||
await smtpService.sendMail({
|
||||
recipients: [reviewerUser?.email as string],
|
||||
subjectLine: "Infisical Secret Change Request",
|
||||
|
||||
substitutions: {
|
||||
firstName: reviewerUser.firstName,
|
||||
projectName: project.name,
|
||||
organizationName: project.organization.name,
|
||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
||||
},
|
||||
template: SmtpTemplates.SecretApprovalRequestNeedsReview
|
||||
});
|
||||
}
|
||||
};
|
@ -53,8 +53,10 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, 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";
|
||||
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
|
||||
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
|
||||
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
|
||||
import {
|
||||
@ -89,7 +91,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
|
||||
projectDAL: Pick<
|
||||
TProjectDALFactory,
|
||||
"checkProjectUpgradeStatus" | "findById" | "findProjectById" | "findProjectWithOrg"
|
||||
>;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
@ -98,6 +103,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
@ -121,6 +127,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
smtpService,
|
||||
userDAL,
|
||||
projectEnvDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
kmsService,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
@ -1061,6 +1068,15 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}
|
||||
return { ...doc, commits: approvalCommits };
|
||||
});
|
||||
|
||||
await sendApprovalEmailsFn({
|
||||
projectDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequest,
|
||||
smtpService,
|
||||
projectId
|
||||
});
|
||||
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
|
||||
@ -1311,8 +1327,17 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return { ...doc, commits: approvalCommits };
|
||||
});
|
||||
|
||||
await sendApprovalEmailsFn({
|
||||
projectDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequest,
|
||||
smtpService,
|
||||
projectId
|
||||
});
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
|
||||
|
@ -964,6 +964,10 @@ export const INTEGRATION = {
|
||||
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
||||
secretGCPLabel: "The label for GCP secrets.",
|
||||
secretAWSTag: "The tags for AWS secrets.",
|
||||
githubVisibility:
|
||||
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
|
||||
githubVisibilityRepoIds:
|
||||
"The repository IDs to sync secrets to when using the Github Integration. Only applicable when using Organization scope, and visibility is set to 'selected'",
|
||||
kmsKeyId: "The ID of the encryption key from AWS KMS.",
|
||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
|
||||
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
|
||||
@ -1120,9 +1124,10 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
certificateChain: "The certificate chain of the issued certificate",
|
||||
serialNumber: "The serial number of the issued certificate"
|
||||
},
|
||||
GET_CRL: {
|
||||
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
||||
crl: "The certificate revocation list (CRL) of the CA"
|
||||
GET_CRLS: {
|
||||
caId: "The ID of the CA to get the certificate revocation lists (CRLs) for",
|
||||
id: "The ID of certificate revocation list (CRL)",
|
||||
crl: "The certificate revocation list (CRL)"
|
||||
}
|
||||
};
|
||||
|
||||
@ -1174,6 +1179,13 @@ export const CERTIFICATE_TEMPLATES = {
|
||||
}
|
||||
};
|
||||
|
||||
export const CA_CRLS = {
|
||||
GET: {
|
||||
crlId: "The ID of the certificate revocation list (CRL) to get",
|
||||
crl: "The certificate revocation list (CRL)"
|
||||
}
|
||||
};
|
||||
|
||||
export const ALERTS = {
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the alert in",
|
||||
|
57
backend/src/lib/axios/digest-auth.ts
Normal file
57
backend/src/lib/axios/digest-auth.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
|
||||
export const createDigestAuthRequestInterceptor = (
|
||||
axiosInstance: AxiosInstance,
|
||||
username: string,
|
||||
password: string
|
||||
) => {
|
||||
let nc = 0;
|
||||
|
||||
return async (opts: AxiosRequestConfig) => {
|
||||
try {
|
||||
return await axiosInstance.request(opts);
|
||||
} catch (err) {
|
||||
const error = err as AxiosError;
|
||||
const authHeader = (error?.response?.headers?.["www-authenticate"] as string) || "";
|
||||
|
||||
if (error?.response?.status !== 401 || !authHeader?.includes("nonce")) {
|
||||
return Promise.reject(error.message);
|
||||
}
|
||||
|
||||
if (!error.config) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const authDetails = authHeader.split(",").map((el) => el.split("="));
|
||||
nc += 1;
|
||||
const nonceCount = nc.toString(16).padStart(8, "0");
|
||||
const cnonce = crypto.randomBytes(24).toString("hex");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
|
||||
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
||||
const path = opts.url;
|
||||
|
||||
const ha2 = crypto
|
||||
.createHash("md5")
|
||||
.update(`${opts.method ?? "GET"}:${path}`)
|
||||
.digest("hex");
|
||||
|
||||
const response = crypto
|
||||
.createHash("md5")
|
||||
.update(`${ha1}:${nonce}:${nonceCount}:${cnonce}:auth:${ha2}`)
|
||||
.digest("hex");
|
||||
const authorization = `Digest username="${username}",realm="${realm}",nonce="${nonce}",uri="${path}",qop="auth",algorithm="MD5",response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
|
||||
|
||||
if (opts.headers) {
|
||||
// eslint-disable-next-line
|
||||
opts.headers.authorization = authorization;
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
opts.headers = { authorization };
|
||||
}
|
||||
return axiosInstance.request(opts);
|
||||
}
|
||||
};
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { Logger } from "pino";
|
||||
import { z } from "zod";
|
||||
|
||||
import { removeTrailingSlash } from "../fn";
|
||||
import { zpStr } from "../zod";
|
||||
|
||||
export const GITLAB_URL = "https://gitlab.com";
|
||||
@ -63,7 +64,9 @@ const envSchema = z
|
||||
.string()
|
||||
.min(32)
|
||||
.default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"),
|
||||
SITE_URL: zpStr(z.string().optional()),
|
||||
|
||||
// Ensure that the SITE_URL never ends with a trailing slash
|
||||
SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional(),
|
||||
// Telemetry
|
||||
TELEMETRY_ENABLED: zodStrBool.default("true"),
|
||||
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
|
||||
@ -74,6 +77,7 @@ const envSchema = z
|
||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
||||
JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")),
|
||||
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
|
||||
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
|
||||
// Oauth
|
||||
@ -141,7 +145,8 @@ const envSchema = z
|
||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||
PLAIN_API_KEY: zpStr(z.string().optional()),
|
||||
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert")
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
121
backend/src/lib/knex/scim.ts
Normal file
121
backend/src/lib/knex/scim.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { Knex } from "knex";
|
||||
import { Compare, Filter, parse } from "scim2-parse-filter";
|
||||
|
||||
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
|
||||
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
|
||||
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
|
||||
}
|
||||
return filter;
|
||||
};
|
||||
|
||||
export const generateKnexQueryFromScim = (
|
||||
rootQuery: Knex.QueryBuilder,
|
||||
rootScimFilter: string,
|
||||
getAttributeField: (attr: string) => string | null
|
||||
) => {
|
||||
const scimRootFilterAst = parse(rootScimFilter);
|
||||
const stack = [
|
||||
{
|
||||
scimFilterAst: scimRootFilterAst,
|
||||
query: rootQuery
|
||||
}
|
||||
];
|
||||
|
||||
while (stack.length) {
|
||||
const { scimFilterAst, query } = stack.pop()!;
|
||||
switch (scimFilterAst.op) {
|
||||
case "eq": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.where(attrPath, scimFilterAst.compValue);
|
||||
break;
|
||||
}
|
||||
case "pr": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereNotNull(attrPath);
|
||||
break;
|
||||
}
|
||||
case "gt": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.where(attrPath, ">", scimFilterAst.compValue);
|
||||
break;
|
||||
}
|
||||
case "ge": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.where(attrPath, ">=", scimFilterAst.compValue);
|
||||
break;
|
||||
}
|
||||
case "lt": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.where(attrPath, "<", scimFilterAst.compValue);
|
||||
break;
|
||||
}
|
||||
case "le": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.where(attrPath, "<=", scimFilterAst.compValue);
|
||||
break;
|
||||
}
|
||||
case "sw": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereILike(attrPath, `${scimFilterAst.compValue}%`);
|
||||
break;
|
||||
}
|
||||
case "ew": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`);
|
||||
break;
|
||||
}
|
||||
case "co": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`);
|
||||
break;
|
||||
}
|
||||
case "ne": {
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue);
|
||||
break;
|
||||
}
|
||||
case "and": {
|
||||
void query.andWhere((subQueryBuilder) => {
|
||||
scimFilterAst.filters.forEach((el) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: el
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "or": {
|
||||
void query.orWhere((subQueryBuilder) => {
|
||||
scimFilterAst.filters.forEach((el) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: el
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "not": {
|
||||
void query.whereNot((subQueryBuilder) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: scimFilterAst.filter
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "[]": {
|
||||
void query.whereNot((subQueryBuilder) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
@ -49,6 +49,21 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
|
||||
server.setValidatorCompiler(validatorCompiler);
|
||||
server.setSerializerCompiler(serializerCompiler);
|
||||
|
||||
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
const strBody = body instanceof Buffer ? body.toString() : body;
|
||||
if (!strBody) {
|
||||
done(null, undefined);
|
||||
return;
|
||||
}
|
||||
const json: unknown = JSON.parse(strBody);
|
||||
done(null, json);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
done(error, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await server.register<FastifyCookieOptions>(cookie, {
|
||||
secret: appCfg.COOKIE_SECRET_SIGN_KEY
|
||||
|
@ -57,7 +57,6 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
|
||||
}
|
||||
const authHeader = req.headers?.authorization;
|
||||
|
||||
if (!authHeader) return { authMode: null, token: null };
|
||||
|
||||
const authTokenValue = authHeader.slice(7); // slice of after Bearer
|
||||
@ -103,12 +102,13 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
server.decorateRequest("auth", null);
|
||||
server.addHook("onRequest", async (req) => {
|
||||
const appCfg = getConfig();
|
||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||
|
||||
if (req.url.includes("/api/v3/auth/")) {
|
||||
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
switch (authMode) {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { CronJob } from "cron";
|
||||
import { Redis } from "ioredis";
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
|
||||
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
||||
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
|
||||
@ -16,6 +18,7 @@ import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audi
|
||||
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||
import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
||||
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
||||
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
||||
@ -71,6 +74,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
|
||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||
@ -91,6 +95,7 @@ import { certificateAuthorityQueueFactory } from "@app/services/certificate-auth
|
||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||
@ -477,9 +482,12 @@ export const registerRoutes = async (
|
||||
orgRoleDAL,
|
||||
permissionService,
|
||||
orgDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
incidentContactDAL,
|
||||
tokenService,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
orgMembershipDAL,
|
||||
@ -499,6 +507,8 @@ export const registerRoutes = async (
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
groupProjectDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
orgDAL,
|
||||
orgService,
|
||||
licenseService
|
||||
@ -595,6 +605,7 @@ export const registerRoutes = async (
|
||||
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||
const certificateTemplateDAL = certificateTemplateDALFactory(db);
|
||||
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
|
||||
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
@ -646,14 +657,29 @@ export const registerRoutes = async (
|
||||
certificateAuthorityCrlDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
licenseService
|
||||
permissionService
|
||||
// licenseService
|
||||
});
|
||||
|
||||
const certificateTemplateService = certificateTemplateServiceFactory({
|
||||
certificateTemplateDAL,
|
||||
certificateTemplateEstConfigDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const certificateEstService = certificateEstServiceFactory({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const pkiAlertService = pkiAlertServiceFactory({
|
||||
@ -683,6 +709,7 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
orgService,
|
||||
projectMembershipDAL,
|
||||
projectRoleDAL,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
certificateAuthorityDAL,
|
||||
@ -839,6 +866,7 @@ export const registerRoutes = async (
|
||||
secretQueueService,
|
||||
kmsService,
|
||||
secretV2BridgeDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
smtpService,
|
||||
@ -1189,6 +1217,7 @@ export const registerRoutes = async (
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
certificateEst: certificateEstService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
secretScanning: secretScanningService,
|
||||
@ -1232,7 +1261,7 @@ export const registerRoutes = async (
|
||||
response: {
|
||||
200: z.object({
|
||||
date: z.date(),
|
||||
message: z.literal("Ok"),
|
||||
message: z.string().optional(),
|
||||
emailConfigured: z.boolean().optional(),
|
||||
inviteOnlySignup: z.boolean().optional(),
|
||||
redisConfigured: z.boolean().optional(),
|
||||
@ -1241,12 +1270,37 @@ export const registerRoutes = async (
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async () => {
|
||||
handler: async (request, reply) => {
|
||||
const cfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
try {
|
||||
await db.raw("SELECT NOW()");
|
||||
} catch (err) {
|
||||
logger.error("Health check: database connection failed", err);
|
||||
return reply.code(503).send({
|
||||
date: new Date(),
|
||||
message: "Service unavailable"
|
||||
});
|
||||
}
|
||||
|
||||
if (cfg.isRedisConfigured) {
|
||||
const redis = new Redis(cfg.REDIS_URL);
|
||||
try {
|
||||
await redis.ping();
|
||||
redis.disconnect();
|
||||
} catch (err) {
|
||||
logger.error("Health check: redis connection failed", err);
|
||||
return reply.code(503).send({
|
||||
date: new Date(),
|
||||
message: "Service unavailable"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
date: new Date(),
|
||||
message: "Ok" as const,
|
||||
message: "Ok",
|
||||
emailConfigured: cfg.isSmtpConfigured,
|
||||
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
||||
redisConfigured: cfg.isRedisConfigured,
|
||||
@ -1256,6 +1310,9 @@ export const registerRoutes = async (
|
||||
}
|
||||
});
|
||||
|
||||
// register special routes
|
||||
await server.register(registerCertificateEstRouter, { prefix: "/.well-known/est" });
|
||||
|
||||
// register routes for v1
|
||||
await server.register(
|
||||
async (v1Server) => {
|
||||
|
@ -669,6 +669,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -691,11 +692,90 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/crls",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get list of CRLs of the CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId)
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
id: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.id),
|
||||
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.crl)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { ca, crls } = await server.services.certificateAuthorityCrl.getCaCrls({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA_CRLS,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return crls;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: implement this endpoint in the future
|
||||
// server.route({
|
||||
// method: "GET",
|
||||
// url: "/:caId/crl/rotate",
|
||||
// config: {
|
||||
// rateLimit: writeLimit
|
||||
// },
|
||||
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
// schema: {
|
||||
// description: "Rotate CRLs of the CA",
|
||||
// params: z.object({
|
||||
// caId: z.string().trim()
|
||||
// }),
|
||||
// response: {
|
||||
// 200: z.object({
|
||||
// message: z.string()
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// handler: async (req) => {
|
||||
// await server.services.certificateAuthority.rotateCaCrl({
|
||||
// caId: req.params.caId,
|
||||
// actor: req.permission.type,
|
||||
// actorId: req.permission.id,
|
||||
// actorAuthMethod: req.permission.authMethod,
|
||||
// actorOrgId: req.permission.orgId
|
||||
// });
|
||||
// return {
|
||||
// message: "Successfully rotated CA CRL"
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
@ -210,6 +210,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -231,7 +232,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -9,6 +10,12 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
||||
id: true,
|
||||
certificateTemplateId: true,
|
||||
isEnabled: true
|
||||
});
|
||||
|
||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@ -202,4 +209,141 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.createEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1).optional(),
|
||||
passphrase: z.string().min(1).optional(),
|
||||
isEnabled: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.updateEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig.extend({
|
||||
caChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: false,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -293,6 +293,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
||||
}),
|
||||
querystring: z.object({
|
||||
teamId: z.string().trim().optional(),
|
||||
azureDevOpsOrgName: z.string().trim().optional(),
|
||||
workspaceSlug: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UsersSchema } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -16,23 +16,37 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
inviteeEmail: z.string().trim().email(),
|
||||
organizationId: z.string().trim()
|
||||
inviteeEmails: z.array(z.string().trim().email()),
|
||||
organizationId: z.string().trim(),
|
||||
projectIds: z.array(z.string().trim()).optional(),
|
||||
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
|
||||
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
completeInviteLink: z.string().optional()
|
||||
completeInviteLinks: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
link: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
if (req.auth.actor !== ActorType.USER) return;
|
||||
const completeInviteLink = await server.services.org.inviteUserToOrganization({
|
||||
|
||||
const completeInviteLinks = await server.services.org.inviteUserToOrganization({
|
||||
orgId: req.body.organizationId,
|
||||
userId: req.permission.id,
|
||||
inviteeEmail: req.body.inviteeEmail,
|
||||
inviteeEmails: req.body.inviteeEmails,
|
||||
projectIds: req.body.projectIds,
|
||||
projectRoleSlug: req.body.projectRoleSlug,
|
||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
@ -41,14 +55,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
event: PostHogEventTypes.UserOrgInvitation,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
inviteeEmail: req.body.inviteeEmail,
|
||||
inviteeEmails: req.body.inviteeEmails,
|
||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
completeInviteLink,
|
||||
message: `Send an invite link to ${req.body.inviteeEmail}`
|
||||
completeInviteLinks,
|
||||
message: `Send an invite link to ${req.body.inviteeEmails.join(", ")}`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import {
|
||||
IntegrationsSchema,
|
||||
ProjectMembershipsSchema,
|
||||
ProjectRolesSchema,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} 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";
|
||||
@ -122,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
includeRoles: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaces: projectWithEnv.array()
|
||||
workspaces: projectWithEnv
|
||||
.extend({
|
||||
roles: ProjectRolesSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const workspaces = await server.services.project.getProjects(req.permission.id);
|
||||
const workspaces = await server.services.project.getProjects({
|
||||
includeRoles: req.query.includeRoles,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { workspaces };
|
||||
}
|
||||
});
|
||||
|
@ -48,7 +48,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
url: "/public/:id",
|
||||
config: {
|
||||
rateLimit: publicEndpointLimit
|
||||
@ -57,38 +57,37 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
hashedHex: z.string().min(1)
|
||||
body: z.object({
|
||||
hashedHex: z.string().min(1),
|
||||
password: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: SecretSharingSchema.pick({
|
||||
encryptedValue: true,
|
||||
iv: true,
|
||||
tag: true,
|
||||
expiresAt: true,
|
||||
expiresAfterViews: true,
|
||||
accessType: true
|
||||
}).extend({
|
||||
orgName: z.string().optional()
|
||||
200: z.object({
|
||||
isPasswordProtected: z.boolean(),
|
||||
secret: SecretSharingSchema.pick({
|
||||
encryptedValue: true,
|
||||
iv: true,
|
||||
tag: true,
|
||||
expiresAt: true,
|
||||
expiresAfterViews: true,
|
||||
accessType: true
|
||||
})
|
||||
.extend({
|
||||
orgName: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
|
||||
const sharedSecret = await req.server.services.secretSharing.getSharedSecretById({
|
||||
sharedSecretId: req.params.id,
|
||||
hashedHex: req.query.hashedHex,
|
||||
hashedHex: req.body.hashedHex,
|
||||
password: req.body.password,
|
||||
orgId: req.permission?.orgId
|
||||
});
|
||||
if (!sharedSecret) return undefined;
|
||||
return {
|
||||
encryptedValue: sharedSecret.encryptedValue,
|
||||
iv: sharedSecret.iv,
|
||||
tag: sharedSecret.tag,
|
||||
expiresAt: sharedSecret.expiresAt,
|
||||
expiresAfterViews: sharedSecret.expiresAfterViews,
|
||||
accessType: sharedSecret.accessType,
|
||||
orgName: sharedSecret.orgName
|
||||
};
|
||||
|
||||
return sharedSecret;
|
||||
}
|
||||
});
|
||||
|
||||
@ -101,6 +100,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
schema: {
|
||||
body: z.object({
|
||||
encryptedValue: z.string(),
|
||||
password: z.string().optional(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
@ -131,6 +131,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().max(50).optional(),
|
||||
password: z.string().optional(),
|
||||
encryptedValue: z.string(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
|
@ -179,7 +179,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim()
|
||||
verifier: z.string().trim(),
|
||||
tokenMetadata: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
|
||||
export enum TokenType {
|
||||
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||
@ -49,3 +51,19 @@ export type TIssueAuthTokenDTO = {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
};
|
||||
|
||||
export enum TokenMetadataType {
|
||||
InviteToProjects = "projects-invite"
|
||||
}
|
||||
|
||||
export type TTokenInviteToProjectsMetadataPayload = {
|
||||
projectIds: string[];
|
||||
projectRoleSlug: ProjectMembershipRole;
|
||||
userId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TTokenMetadata = {
|
||||
type: TokenMetadataType.InviteToProjects;
|
||||
payload: TTokenInviteToProjectsMetadataPayload;
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -17,9 +17,12 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TAuthDALFactory } from "./auth-dal";
|
||||
@ -32,10 +35,14 @@ type TAuthSignupDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
|
||||
| "find"
|
||||
| "transaction"
|
||||
| "insertMany"
|
||||
| "deletePendingUserGroupMembershipsByUserIds"
|
||||
| "findUserGroupMembershipsInProject"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
@ -43,6 +50,8 @@ type TAuthSignupDep = {
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
};
|
||||
|
||||
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
||||
@ -58,6 +67,8 @@ export const authSignupServiceFactory = ({
|
||||
smtpService,
|
||||
orgService,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
licenseService
|
||||
}: TAuthSignupDep) => {
|
||||
// first step of signup. create user and send email
|
||||
@ -301,7 +312,8 @@ export const authSignupServiceFactory = ({
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
authorization
|
||||
authorization,
|
||||
tokenMetadata
|
||||
}: TCompleteAccountInviteDTO) => {
|
||||
const user = await userDAL.findUserByUsername(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
@ -358,6 +370,45 @@ export const authSignupServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (tokenMetadata) {
|
||||
const metadataObj = jwt.verify(tokenMetadata, appCfg.AUTH_SECRET) as TTokenMetadata;
|
||||
|
||||
if (
|
||||
metadataObj?.payload?.userId !== user.id ||
|
||||
metadataObj?.payload?.orgId !== orgMembership.orgId ||
|
||||
metadataObj?.type !== TokenMetadataType.InviteToProjects
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Malformed or invalid metadata token"
|
||||
});
|
||||
}
|
||||
|
||||
for await (const projectId of metadataObj.payload.projectIds) {
|
||||
await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject(
|
||||
{
|
||||
emails: [user.email!],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: metadataObj.payload.projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
{
|
||||
tx,
|
||||
throwOnProjectNotFound: false
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedMembersips = await orgDAL.updateMembership(
|
||||
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
||||
{ userId: us.id, status: OrgMembershipStatus.Accepted },
|
||||
|
@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
authorization: string;
|
||||
tokenMetadata?: string;
|
||||
};
|
||||
|
@ -13,6 +13,13 @@ import {
|
||||
TRebuildCaCrlDTO
|
||||
} from "./certificate-authority-types";
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
export const createSerialNumber = () => {
|
||||
const randomBytes = crypto.randomBytes(32);
|
||||
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
||||
return randomBytes.toString("hex");
|
||||
};
|
||||
|
||||
export const createDistinguishedName = (parts: TDNParts) => {
|
||||
const dnParts = [];
|
||||
if (parts.country) dnParts.push(`C=${parts.country}`);
|
||||
@ -284,12 +291,11 @@ export const rebuildCaCrl = async ({
|
||||
thisUpdate: new Date(),
|
||||
nextUpdate: new Date("2025/12/12"),
|
||||
entries: revokedCerts.map((revokedCert) => {
|
||||
const revocationDate = new Date(revokedCert.revokedAt as Date);
|
||||
return {
|
||||
serialNumber: revokedCert.serialNumber,
|
||||
revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||
reason: revokedCert.revocationReason as number,
|
||||
invalidity: new Date("2022/01/01"),
|
||||
issuer: ca.dn
|
||||
revocationDate,
|
||||
reason: revokedCert.revocationReason as number
|
||||
};
|
||||
}),
|
||||
signingAlgorithm: alg,
|
||||
|
@ -8,6 +8,7 @@ import { z } from "zod";
|
||||
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
@ -25,6 +26,7 @@ import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cer
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import {
|
||||
createDistinguishedName,
|
||||
createSerialNumber,
|
||||
getCaCertChain, // TODO: consider rename
|
||||
getCaCertChains,
|
||||
getCaCredentials,
|
||||
@ -147,7 +149,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
? new Date(notAfter)
|
||||
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const serialNumber = createSerialNumber();
|
||||
|
||||
const ca = await certificateAuthorityDAL.create(
|
||||
{
|
||||
@ -263,7 +265,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await certificateAuthorityCrlDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
encryptedCrl
|
||||
encryptedCrl,
|
||||
caSecretId: caSecret.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -433,7 +436,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
// get latest CA certificate
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const serialNumber = createSerialNumber();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -846,7 +849,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const serialNumber = createSerialNumber();
|
||||
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
@ -1142,7 +1145,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||
});
|
||||
|
||||
const { caPrivateKey } = await getCaCredentials({
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
@ -1150,9 +1153,15 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
@ -1203,7 +1212,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
@ -1286,24 +1295,23 @@ export const certificateAuthorityServiceFactory = ({
|
||||
* Return new leaf certificate issued by CA with id [caId].
|
||||
* Note: CSR is generated externally and submitted to Infisical.
|
||||
*/
|
||||
const signCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignCertFromCaDTO) => {
|
||||
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
|
||||
const {
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter
|
||||
} = dto;
|
||||
|
||||
let collectionId = pkiCollectionId;
|
||||
|
||||
if (caId) {
|
||||
@ -1324,15 +1332,20 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
ca.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
}
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
@ -1373,6 +1386,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notAfterDate = new Date(notAfter);
|
||||
} else if (ttl) {
|
||||
notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||
} else if (certificateTemplate?.ttl) {
|
||||
notAfterDate = new Date(new Date().getTime() + ms(certificateTemplate.ttl));
|
||||
}
|
||||
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
@ -1417,6 +1432,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesFromCsr: string = "";
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
@ -1445,7 +1461,24 @@ export const certificateAuthorityServiceFactory = ({
|
||||
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
||||
throw new Error(`Invalid altName: ${altName}`);
|
||||
});
|
||||
} else {
|
||||
// attempt to read from CSR if altNames is not explicitly provided
|
||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (sanExtension) {
|
||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||
|
||||
altNamesArray = sanNames.items
|
||||
.filter((value) => value.type === "email" || value.type === "dns")
|
||||
.map((name) => ({
|
||||
type: name.type as "email" | "dns",
|
||||
value: name.value
|
||||
}));
|
||||
|
||||
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
|
||||
}
|
||||
}
|
||||
|
||||
if (altNamesArray.length) {
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
@ -1462,7 +1495,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
@ -1491,7 +1524,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || csrObj.subject,
|
||||
commonName: cn,
|
||||
altNames,
|
||||
altNames: altNamesFromCsr || altNames,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
@ -1529,7 +1562,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificate: leafCert,
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
|
@ -97,18 +97,33 @@ export type TIssueCertFromCaDTO = {
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO = {
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
export type TSignCertFromCaDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames?: string;
|
||||
ttl?: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TDNParts = {
|
||||
commonName?: string;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
@ -30,20 +32,21 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (id: string) => {
|
||||
const getById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const certTemplate = await db
|
||||
.replicaNode()(TableName.CertificateTemplate)
|
||||
const certTemplate = await (tx || db.replicaNode())(TableName.CertificateTemplate)
|
||||
.join(
|
||||
TableName.CertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
`${TableName.CertificateTemplate}.caId`
|
||||
)
|
||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`)
|
||||
.where(`${TableName.CertificateTemplate}.id`, "=", id)
|
||||
.select(selectAllTableCols(TableName.CertificateTemplate))
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.CertificateAuthority),
|
||||
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority)
|
||||
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
|
||||
db.ref("orgId").withSchema(TableName.Project)
|
||||
)
|
||||
.first();
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateTemplateEstConfigDALFactory = ReturnType<typeof certificateTemplateEstConfigDALFactory>;
|
||||
|
||||
export const certificateTemplateEstConfigDALFactory = (db: TDbClient) => {
|
||||
const certificateTemplateEstConfigOrm = ormify(db, TableName.CertificateTemplateEstConfig);
|
||||
|
||||
return certificateTemplateEstConfigOrm;
|
||||
};
|
@ -1,30 +1,51 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
|
||||
import { TCertificateTemplateEstConfigDALFactory } from "./certificate-template-est-config-dal";
|
||||
import {
|
||||
TCreateCertTemplateDTO,
|
||||
TCreateEstConfigurationDTO,
|
||||
TDeleteCertTemplateDTO,
|
||||
TGetCertTemplateDTO,
|
||||
TUpdateCertTemplateDTO
|
||||
TGetEstConfigurationDTO,
|
||||
TUpdateCertTemplateDTO,
|
||||
TUpdateEstConfigurationDTO
|
||||
} from "./certificate-template-types";
|
||||
|
||||
type TCertificateTemplateServiceFactoryDep = {
|
||||
certificateTemplateDAL: TCertificateTemplateDALFactory;
|
||||
certificateTemplateEstConfigDAL: TCertificateTemplateEstConfigDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTemplateServiceFactory>;
|
||||
|
||||
export const certificateTemplateServiceFactory = ({
|
||||
certificateTemplateDAL,
|
||||
certificateTemplateEstConfigDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL,
|
||||
licenseService
|
||||
}: TCertificateTemplateServiceFactoryDep) => {
|
||||
const createCertTemplate = async ({
|
||||
caId,
|
||||
@ -57,23 +78,28 @@ export const certificateTemplateServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const { id } = await certificateTemplateDAL.create({
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
return certificateTemplateDAL.transaction(async (tx) => {
|
||||
const { id } = await certificateTemplateDAL.create(
|
||||
{
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const certificateTemplate = await certificateTemplateDAL.getById(id, tx);
|
||||
if (!certificateTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
return certificateTemplate;
|
||||
});
|
||||
|
||||
const certificateTemplate = await certificateTemplateDAL.getById(id);
|
||||
if (!certificateTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
return certificateTemplate;
|
||||
};
|
||||
|
||||
const updateCertTemplate = async ({
|
||||
@ -118,23 +144,29 @@ export const certificateTemplateServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
await certificateTemplateDAL.updateById(certTemplate.id, {
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
name,
|
||||
ttl
|
||||
return certificateTemplateDAL.transaction(async (tx) => {
|
||||
await certificateTemplateDAL.updateById(
|
||||
certTemplate.id,
|
||||
{
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
name,
|
||||
ttl
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const updatedTemplate = await certificateTemplateDAL.getById(id, tx);
|
||||
if (!updatedTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
return updatedTemplate;
|
||||
});
|
||||
|
||||
const updatedTemplate = await certificateTemplateDAL.getById(id);
|
||||
if (!updatedTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
return updatedTemplate;
|
||||
};
|
||||
|
||||
const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => {
|
||||
@ -187,10 +219,243 @@ export const certificateTemplateServiceFactory = ({
|
||||
return certTemplate;
|
||||
};
|
||||
|
||||
const createEstConfiguration = async ({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateEstConfigurationDTO) => {
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.pkiEst) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create EST configuration due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
const estConfig = await certificateTemplateEstConfigDAL.create({
|
||||
certificateTemplateId,
|
||||
hashedPassphrase,
|
||||
encryptedCaChain,
|
||||
isEnabled
|
||||
});
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
};
|
||||
|
||||
const updateEstConfiguration = async ({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateEstConfigurationDTO) => {
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.pkiEst) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update EST configuration due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!originalCaEstConfig) {
|
||||
throw new NotFoundError({
|
||||
message: "EST configuration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const updatedData: TCertificateTemplateEstConfigsUpdate = {
|
||||
isEnabled
|
||||
};
|
||||
|
||||
if (caChain) {
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
updatedData.encryptedCaChain = encryptedCaChain;
|
||||
}
|
||||
|
||||
if (passphrase) {
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
updatedData.hashedPassphrase = hashedPassphrase;
|
||||
}
|
||||
|
||||
const estConfig = await certificateTemplateEstConfigDAL.updateById(originalCaEstConfig.id, updatedData);
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
};
|
||||
|
||||
const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => {
|
||||
const { certificateTemplateId } = dto;
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
certTemplate.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
}
|
||||
|
||||
const estConfig = await certificateTemplateEstConfigDAL.findOne({
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig) {
|
||||
throw new NotFoundError({
|
||||
message: "EST configuration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaChain = await kmsDecryptor({
|
||||
cipherTextBlob: estConfig.encryptedCaChain
|
||||
});
|
||||
|
||||
return {
|
||||
certificateTemplateId,
|
||||
id: estConfig.id,
|
||||
isEnabled: estConfig.isEnabled,
|
||||
caChain: decryptedCaChain.toString(),
|
||||
hashedPassphrase: estConfig.hashedPassphrase,
|
||||
projectId: certTemplate.projectId,
|
||||
orgId: certTemplate.orgId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createCertTemplate,
|
||||
getCertTemplate,
|
||||
deleteCertTemplate,
|
||||
updateCertTemplate
|
||||
updateCertTemplate,
|
||||
createEstConfiguration,
|
||||
updateEstConfiguration,
|
||||
getEstConfiguration
|
||||
};
|
||||
};
|
||||
|
@ -26,3 +26,27 @@ export type TGetCertTemplateDTO = {
|
||||
export type TDeleteCertTemplateDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetEstConfigurationDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
certificateTemplateId: string;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
certificateTemplateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
@ -24,3 +24,19 @@ export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||
return x509.X509CrlReason.unspecified;
|
||||
}
|
||||
};
|
||||
|
||||
export const isCertChainValid = async (certificates: x509.X509Certificate[]) => {
|
||||
if (certificates.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const leafCert = certificates[0];
|
||||
const chain = new x509.X509ChainBuilder({
|
||||
certificates: certificates.slice(1)
|
||||
});
|
||||
|
||||
const chainItems = await chain.build(leafCert);
|
||||
|
||||
// chain.build() implicitly verifies the chain
|
||||
return chainItems.length === certificates.length;
|
||||
};
|
||||
|
@ -1030,11 +1030,31 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
|
||||
return apps;
|
||||
};
|
||||
|
||||
const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: string; orgName: string }) => {
|
||||
const res = (
|
||||
await request.get<{ count: number; value: Record<string, string>[] }>(
|
||||
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/projects?api-version=7.2-preview.2`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Basic ${accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
const apps = res.value.map((a) => ({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
}));
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
export const getApps = async ({
|
||||
integration,
|
||||
accessToken,
|
||||
accessId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug,
|
||||
url
|
||||
}: {
|
||||
@ -1042,6 +1062,7 @@ export const getApps = async ({
|
||||
accessToken: string;
|
||||
accessId?: string;
|
||||
teamId?: string | null;
|
||||
azureDevOpsOrgName?: string | null;
|
||||
workspaceSlug?: string;
|
||||
url?: string | null;
|
||||
}): Promise<App[]> => {
|
||||
@ -1184,6 +1205,12 @@ export const getApps = async ({
|
||||
accessToken
|
||||
});
|
||||
|
||||
case Integrations.AZURE_DEVOPS:
|
||||
return getAppsAzureDevOps({
|
||||
accessToken,
|
||||
orgName: azureDevOpsOrgName as string
|
||||
});
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "integration not found" });
|
||||
}
|
||||
|
@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
id,
|
||||
workspaceSlug
|
||||
}: TIntegrationAuthAppsDTO) => {
|
||||
@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({
|
||||
accessToken,
|
||||
accessId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug,
|
||||
url: integrationAuth.url
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TIntegrations } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetIntegrationAuthDTO = {
|
||||
@ -28,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
|
||||
export type TIntegrationAuthAppsDTO = {
|
||||
id: string;
|
||||
teamId?: string;
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@ -163,3 +165,13 @@ export type TTeamCityBuildConfig = {
|
||||
href: string;
|
||||
webUrl: string;
|
||||
};
|
||||
|
||||
export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||
environment?:
|
||||
| {
|
||||
id?: string | null | undefined;
|
||||
name?: string | null | undefined;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
};
|
||||
|
@ -31,7 +31,8 @@ export enum Integrations {
|
||||
CLOUD_66 = "cloud-66",
|
||||
NORTHFLANK = "northflank",
|
||||
HASURA_CLOUD = "hasura-cloud",
|
||||
RUNDECK = "rundeck"
|
||||
RUNDECK = "rundeck",
|
||||
AZURE_DEVOPS = "azure-devops"
|
||||
}
|
||||
|
||||
export enum IntegrationType {
|
||||
@ -88,6 +89,7 @@ export enum IntegrationUrls {
|
||||
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
||||
NORTHFLANK_API_URL = "https://api.northflank.com",
|
||||
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
|
||||
AZURE_DEVOPS_API_URL = "https://dev.azure.com",
|
||||
|
||||
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
|
||||
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||
@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => {
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Azure DevOps",
|
||||
slug: "azure-devops",
|
||||
image: "Microsoft Azure.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -35,6 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
@ -1624,7 +1625,11 @@ const syncSecretsGitHub = async ({
|
||||
await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
|
||||
org: integration.owner as string,
|
||||
secret_name: key,
|
||||
visibility: "all",
|
||||
visibility: metadata.githubVisibility ?? "all",
|
||||
...(metadata.githubVisibility === "selected" && {
|
||||
// we need to map the githubVisibilityRepoIds to numbers
|
||||
selected_repository_ids: metadata.githubVisibilityRepoIds?.map(Number) ?? []
|
||||
}),
|
||||
encrypted_value: encryptedSecret,
|
||||
key_id: repoPublicKey.key_id
|
||||
});
|
||||
@ -2075,6 +2080,116 @@ const syncSecretsTravisCI = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||
*/
|
||||
const syncSecretsAzureDevops = async ({
|
||||
integrationAuth,
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: TIntegrationAuths;
|
||||
integration: TIntegrationsWithEnvironment;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
if (!integration.appId || !integration.app) {
|
||||
throw new Error("Azure DevOps: orgId and projectId are required");
|
||||
}
|
||||
if (!integration.environment || !integration.environment.name) {
|
||||
throw new Error("Azure DevOps: environment is required");
|
||||
}
|
||||
const headers = {
|
||||
Authorization: `Basic ${accessToken}`
|
||||
};
|
||||
const azureDevopsApiUrl = integrationAuth.url ? `${integrationAuth.url}` : IntegrationUrls.AZURE_DEVOPS_API_URL;
|
||||
|
||||
const getEnvGroupId = async (orgId: string, project: string, env: string) => {
|
||||
let groupId;
|
||||
const url: string | null =
|
||||
`${azureDevopsApiUrl}/${orgId}/${project}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
|
||||
|
||||
const response = await request.get(url, { headers });
|
||||
for (const group of response.data.value) {
|
||||
const groupName = group.name;
|
||||
if (groupName === env) {
|
||||
groupId = group.id;
|
||||
return { groupId, groupName };
|
||||
}
|
||||
}
|
||||
return { groupId: "", groupName: "" };
|
||||
};
|
||||
|
||||
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
|
||||
|
||||
const variables: Record<string, { value: string }> = {};
|
||||
for (const key of Object.keys(secrets)) {
|
||||
variables[key] = { value: secrets[key].value };
|
||||
}
|
||||
|
||||
if (!groupId) {
|
||||
// create new variable group if not present
|
||||
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
|
||||
const config = {
|
||||
method: "POST",
|
||||
url,
|
||||
data: {
|
||||
name: integration.environment.name,
|
||||
description: integration.environment.name,
|
||||
type: "Vsts",
|
||||
owner: "Library",
|
||||
variables,
|
||||
variableGroupProjectReferences: [
|
||||
{
|
||||
name: integration.environment.name,
|
||||
projectReference: {
|
||||
name: integration.appId
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
headers: {
|
||||
headers
|
||||
}
|
||||
};
|
||||
|
||||
const res = await request.post(url, config.data, config.headers);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Azure DevOps: Failed to create variable group: ${res.statusText}`);
|
||||
}
|
||||
} else {
|
||||
// sync variables for pre-existing variable group
|
||||
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.2-preview.2`;
|
||||
const config = {
|
||||
method: "PUT",
|
||||
url,
|
||||
data: {
|
||||
name: groupName,
|
||||
description: groupName,
|
||||
type: "Vsts",
|
||||
owner: "Library",
|
||||
variables,
|
||||
variableGroupProjectReferences: [
|
||||
{
|
||||
name: groupName,
|
||||
projectReference: {
|
||||
name: integration.appId
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
headers: {
|
||||
headers
|
||||
}
|
||||
};
|
||||
const res = await request.put(url, config.data, config.headers);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Azure DevOps: Failed to update variable group: ${res.statusText}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||
*/
|
||||
@ -3714,6 +3829,15 @@ export const syncIntegrationSecrets = async ({
|
||||
updateManySecretsRawFn
|
||||
});
|
||||
break;
|
||||
|
||||
case Integrations.AZURE_DEVOPS:
|
||||
await syncSecretsAzureDevops({
|
||||
integrationAuth,
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case Integrations.AWS_PARAMETER_STORE:
|
||||
response = await syncSecretsAWSParameterStore({
|
||||
integration,
|
||||
|
@ -5,14 +5,18 @@ import { INTEGRATION } from "@app/lib/api-docs";
|
||||
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
|
||||
|
||||
export const IntegrationMetadataSchema = z.object({
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
|
||||
mappingBehavior: z
|
||||
.nativeEnum(IntegrationMappingBehavior)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
@ -20,6 +24,7 @@ export const IntegrationMetadataSchema = z.object({
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
|
||||
|
||||
secretAWSTag: z
|
||||
.array(
|
||||
z.object({
|
||||
@ -29,7 +34,15 @@ export const IntegrationMetadataSchema = z.object({
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
|
||||
githubVisibility: z
|
||||
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.githubVisibility),
|
||||
githubVisibilityRepoIds: z.array(z.string()).optional().describe(INTEGRATION.CREATE.metadata.githubVisibilityRepoIds),
|
||||
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
|
||||
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
|
||||
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
|
||||
|
@ -27,6 +27,10 @@ export type TCreateIntegrationDTO = {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
|
||||
githubVisibility?: string;
|
||||
githubVisibilityRepoIds?: string[];
|
||||
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldMaskSecrets?: boolean;
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
|
||||
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
|
||||
|
||||
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
|
||||
|
||||
@ -114,10 +115,11 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => {
|
||||
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
|
||||
try {
|
||||
const members = await db
|
||||
.replicaNode()(TableName.OrgMembership)
|
||||
const conn = tx || db;
|
||||
const members = await conn(TableName.OrgMembership)
|
||||
// .replicaNode()(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
@ -126,18 +128,18 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.OrgMembership),
|
||||
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
db.ref("role").withSchema(TableName.OrgMembership),
|
||||
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
db.ref("status").withSchema(TableName.OrgMembership),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
conn.ref("id").withSchema(TableName.OrgMembership),
|
||||
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
conn.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("role").withSchema(TableName.OrgMembership),
|
||||
conn.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("status").withSchema(TableName.OrgMembership),
|
||||
conn.ref("username").withSchema(TableName.Users),
|
||||
conn.ref("email").withSchema(TableName.Users),
|
||||
conn.ref("firstName").withSchema(TableName.Users),
|
||||
conn.ref("lastName").withSchema(TableName.Users),
|
||||
conn.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.whereIn("username", usernames);
|
||||
@ -279,6 +281,67 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("isEmailVerified").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("scimEnabled").withSchema(TableName.Organization),
|
||||
db.ref("externalId").withSchema(TableName.UserAliases)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
const res = await query;
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
};
|
||||
|
||||
const findMembershipWithScimFilter = async (
|
||||
orgId: string,
|
||||
scimFilter: string | undefined,
|
||||
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.OrgMembership)
|
||||
// eslint-disable-next-line
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.where((qb) => {
|
||||
if (scimFilter) {
|
||||
void generateKnexQueryFromScim(qb, scimFilter, (attrPath) => {
|
||||
switch (attrPath) {
|
||||
case "active":
|
||||
return `${TableName.OrgMembership}.isActive`;
|
||||
case "userName":
|
||||
return `${TableName.UserAliases}.externalId`;
|
||||
case "name.givenName":
|
||||
return `${TableName.Users}.firstName`;
|
||||
case "name.familyName":
|
||||
return `${TableName.Users}.lastName`;
|
||||
case "email.value":
|
||||
return `${TableName.Users}.email`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||
.leftJoin(TableName.UserAliases, function joinUserAlias() {
|
||||
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
|
||||
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
|
||||
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
|
||||
})
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("isEmailVerified").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
@ -313,6 +376,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
updateById,
|
||||
deleteById,
|
||||
findMembership,
|
||||
findMembershipWithScimFilter,
|
||||
createMembership,
|
||||
updateMembershipById,
|
||||
deleteMembershipById,
|
||||
|
@ -4,9 +4,17 @@ import crypto from "crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@ -24,10 +32,14 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
|
||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { verifyProjectVersions } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||
@ -56,8 +68,11 @@ type TOrgServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
groupDAL: TGroupDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
||||
projectMembershipDAL: Pick<
|
||||
TProjectMembershipDALFactory,
|
||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
|
||||
incidentContactDAL: TIncidentContactsDALFactory;
|
||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||
@ -69,6 +84,9 @@ type TOrgServiceFactoryDep = {
|
||||
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||
>;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
};
|
||||
|
||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||
@ -90,7 +108,10 @@ export const orgServiceFactory = ({
|
||||
tokenService,
|
||||
orgBotDAL,
|
||||
licenseService,
|
||||
samlConfigDAL
|
||||
samlConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
/*
|
||||
* Get organization details by the organization id
|
||||
@ -420,10 +441,15 @@ export const orgServiceFactory = ({
|
||||
const inviteUserToOrganization = async ({
|
||||
orgId,
|
||||
userId,
|
||||
inviteeEmail,
|
||||
inviteeEmails,
|
||||
organizationRoleSlug,
|
||||
projectRoleSlug,
|
||||
projectIds,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TInviteUserToOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
|
||||
@ -450,98 +476,203 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const invitee = await orgDAL.transaction(async (tx) => {
|
||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite an existing member of org",
|
||||
name: "Invite user to org"
|
||||
});
|
||||
if (projectIds?.length) {
|
||||
const projects = await projectDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: projectIds
|
||||
}
|
||||
});
|
||||
|
||||
if (!inviteeMembership) {
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return inviteeUser;
|
||||
}
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
||||
if (isEmailInvalid) {
|
||||
// if its not v3, throw an error
|
||||
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||
});
|
||||
}
|
||||
// not invited before
|
||||
const user = await userDAL.create(
|
||||
{
|
||||
username: inviteeEmail,
|
||||
email: inviteeEmail,
|
||||
isAccepted: false,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
||||
const users: Pick<
|
||||
TUsers & { orgId: string },
|
||||
"id" | "firstName" | "lastName" | "email" | "orgId" | "username"
|
||||
>[] = [];
|
||||
for await (const inviteeEmail of inviteeEmails) {
|
||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to invite members because ${inviteeEmail} is already part of the organization`,
|
||||
name: "Invite user to org"
|
||||
});
|
||||
}
|
||||
|
||||
if (!inviteeMembership) {
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (projectIds?.length) {
|
||||
if (
|
||||
organizationRoleSlug === OrgMembershipRole.Custom ||
|
||||
projectRoleSlug === ProjectMembershipRole.Custom
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Custom roles are not supported for inviting users to projects and organizations"
|
||||
});
|
||||
}
|
||||
|
||||
if (!projectRoleSlug) {
|
||||
throw new BadRequestError({
|
||||
message: "Selecting a project role is required to invite users to projects"
|
||||
});
|
||||
}
|
||||
|
||||
await projectMembershipDAL.insertMany(
|
||||
projectIds.map((id) => ({ projectId: id, userId: inviteeUser.id })),
|
||||
tx
|
||||
);
|
||||
for await (const projectId of projectIds) {
|
||||
await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject(
|
||||
{
|
||||
emails: [inviteeEmail],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
{
|
||||
tx
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [{ ...inviteeUser, orgId }];
|
||||
}
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
||||
if (isEmailInvalid) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
});
|
||||
}
|
||||
// not invited before
|
||||
const user = await userDAL.create(
|
||||
{
|
||||
username: inviteeEmail,
|
||||
email: inviteeEmail,
|
||||
isAccepted: false,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: organizationRoleSlug,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
users.push({
|
||||
...user,
|
||||
orgId
|
||||
});
|
||||
}
|
||||
return users;
|
||||
});
|
||||
|
||||
const user = await userDAL.findById(userId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [inviteeEmail],
|
||||
substitutions: {
|
||||
inviterFirstName: user.firstName,
|
||||
inviterUsername: user.username,
|
||||
organizationName: org?.name,
|
||||
email: inviteeEmail,
|
||||
organizationId: org?.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
|
||||
const signupTokens: { email: string; link: string }[] = [];
|
||||
if (inviteeUsers) {
|
||||
for await (const invitee of inviteeUsers) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
});
|
||||
|
||||
let inviteMetadata: string = "";
|
||||
if (projectIds && projectIds?.length > 0) {
|
||||
inviteMetadata = jwt.sign(
|
||||
{
|
||||
type: TokenMetadataType.InviteToProjects,
|
||||
payload: {
|
||||
projectIds,
|
||||
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
}
|
||||
} satisfies TTokenMetadata,
|
||||
appCfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn: appCfg.JWT_INVITE_LIFETIME
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
signupTokens.push({
|
||||
email: invitee.email || invitee.username,
|
||||
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${
|
||||
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
|
||||
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [invitee.email || invitee.username],
|
||||
substitutions: {
|
||||
metadata: inviteMetadata,
|
||||
inviterFirstName: user.firstName,
|
||||
inviterUsername: user.username,
|
||||
organizationName: org?.name,
|
||||
email: invitee.email || invitee.username,
|
||||
organizationId: org?.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
|
||||
if (!appCfg.isSmtpConfigured) {
|
||||
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
|
||||
return signupTokens;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@ -29,7 +30,10 @@ export type TInviteUserToOrgDTO = {
|
||||
orgId: string;
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
inviteeEmail: string;
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug: OrgMembershipRole;
|
||||
projectIds?: string[];
|
||||
projectRoleSlug?: ProjectMembershipRole;
|
||||
};
|
||||
|
||||
export type TVerifyUserToOrgDTO = {
|
||||
|
@ -0,0 +1,190 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
||||
|
||||
type TAddMembersToProjectArg = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectById" | "findProjectGhostUser">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "insertMany">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
type AddMembersToNonE2EEProjectDTO = {
|
||||
emails: string[];
|
||||
usernames: string[];
|
||||
projectId: string;
|
||||
projectMembershipRole: ProjectMembershipRole;
|
||||
sendEmails?: boolean;
|
||||
};
|
||||
|
||||
type AddMembersToNonE2EEProjectOptions = {
|
||||
tx?: Knex;
|
||||
throwOnProjectNotFound?: boolean;
|
||||
};
|
||||
|
||||
export const addMembersToProject = ({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}: TAddMembersToProjectArg) => {
|
||||
// Can create multiple memberships for a singular project, based on user email / username
|
||||
const addMembersToNonE2EEProject = async (
|
||||
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
||||
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
||||
) => {
|
||||
const processTransaction = async (tx: Knex) => {
|
||||
const usernamesAndEmails = [...emails, ...usernames];
|
||||
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
if (!project) {
|
||||
if (options.throwOnProjectNotFound) {
|
||||
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const orgMembers = await orgDAL.findOrgMembersByUsername(
|
||||
project.orgId,
|
||||
[...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))],
|
||||
tx
|
||||
);
|
||||
|
||||
if (orgMembers.length !== usernamesAndEmails.length)
|
||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
|
||||
if (!orgMembers.length) return [];
|
||||
|
||||
const existingMembers = await projectMembershipDAL.find({
|
||||
projectId,
|
||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||
});
|
||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: orgMembers.map((membership) => ({
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole,
|
||||
userPublicKey: membership.user.publicKey
|
||||
}))
|
||||
});
|
||||
|
||||
const members: TProjectMemberships[] = [];
|
||||
|
||||
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||
);
|
||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||
orgMembers.map(({ user }) => ({
|
||||
projectId,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.insertMany(
|
||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
||||
tx
|
||||
);
|
||||
|
||||
members.push(...projectMemberships);
|
||||
|
||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||
await projectKeyDAL.insertMany(
|
||||
orgMembers
|
||||
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
||||
.map(({ user, id }) => ({
|
||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.id,
|
||||
projectId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
if (sendEmails) {
|
||||
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (recipients.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical project invitation",
|
||||
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
if (options.tx) {
|
||||
return processTransaction(options.tx);
|
||||
}
|
||||
return projectMembershipDAL.transaction(processTransaction);
|
||||
};
|
||||
|
||||
return {
|
||||
addMembersToNonE2EEProject
|
||||
};
|
||||
};
|
@ -2,19 +2,12 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TProjectMemberships
|
||||
} from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
|
||||
@ -23,13 +16,13 @@ import { ActorType } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import { addMembersToProject } from "./project-membership-fns";
|
||||
import {
|
||||
ProjectUserMembershipTemporaryMode,
|
||||
TAddUsersToWorkspaceDTO,
|
||||
@ -53,7 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
@ -247,116 +240,23 @@ export const projectMembershipServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
|
||||
const usernamesAndEmails = [...emails, ...usernames];
|
||||
|
||||
const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [
|
||||
...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))
|
||||
]);
|
||||
|
||||
if (orgMembers.length !== usernamesAndEmails.length)
|
||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
|
||||
if (!orgMembers.length) return [];
|
||||
|
||||
const existingMembers = await projectMembershipDAL.find({
|
||||
const members = await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject({
|
||||
emails,
|
||||
usernames,
|
||||
projectId,
|
||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||
});
|
||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
sendEmails
|
||||
});
|
||||
|
||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: orgMembers.map((membership) => ({
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
userPublicKey: membership.user.publicKey
|
||||
}))
|
||||
});
|
||||
|
||||
const members: TProjectMemberships[] = [];
|
||||
|
||||
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||
);
|
||||
|
||||
await projectMembershipDAL.transaction(async (tx) => {
|
||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||
orgMembers.map(({ user }) => ({
|
||||
projectId,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.insertMany(
|
||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
|
||||
tx
|
||||
);
|
||||
|
||||
members.push(...projectMemberships);
|
||||
|
||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||
await projectKeyDAL.insertMany(
|
||||
orgMembers
|
||||
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
||||
.map(({ user, id }) => ({
|
||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.id,
|
||||
projectId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (recipients.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical project invitation",
|
||||
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return members;
|
||||
};
|
||||
|
||||
|
52
backend/src/services/project-role/project-role-fns.ts
Normal file
52
backend/src/services/project-role/project-role-fns.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import {
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
projectViewerPermission
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
|
||||
export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
|
||||
return [
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
||||
projectId,
|
||||
name: "Admin",
|
||||
slug: ProjectMembershipRole.Admin,
|
||||
permissions: projectAdminPermissions,
|
||||
description: "Full administrative access over a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Developer",
|
||||
slug: ProjectMembershipRole.Member,
|
||||
permissions: projectMemberPermissions,
|
||||
description: "Limited read/write role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Viewer",
|
||||
slug: ProjectMembershipRole.Viewer,
|
||||
permissions: projectViewerPermission,
|
||||
description: "Only read role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "No Access",
|
||||
slug: ProjectMembershipRole.NoAccess,
|
||||
permissions: projectNoAccessPermissions,
|
||||
description: "No access to any resources in the project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
||||
};
|
@ -5,13 +5,9 @@ import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub,
|
||||
projectViewerPermission
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||
import { getPredefinedRoles } from "./project-role-fns";
|
||||
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
||||
|
||||
type TProjectRoleServiceFactoryDep = {
|
||||
@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
|
||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||
);
|
||||
|
||||
const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
|
||||
return [
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
||||
projectId,
|
||||
name: "Admin",
|
||||
slug: ProjectMembershipRole.Admin,
|
||||
permissions: projectAdminPermissions,
|
||||
description: "Full administrative access over a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Developer",
|
||||
slug: ProjectMembershipRole.Member,
|
||||
permissions: projectMemberPermissions,
|
||||
description: "Limited read/write role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Viewer",
|
||||
slug: ProjectMembershipRole.Viewer,
|
||||
permissions: projectViewerPermission,
|
||||
description: "Only read role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "No Access",
|
||||
slug: ProjectMembershipRole.NoAccess,
|
||||
permissions: projectNoAccessPermissions,
|
||||
description: "No access to any resources in the project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
||||
};
|
||||
|
||||
export const projectRoleServiceFactory = ({
|
||||
projectRoleDAL,
|
||||
permissionService,
|
||||
|
@ -279,6 +279,34 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectWithOrg = async (projectId: string) => {
|
||||
// we just need the project, and we need to include a new .organization field that includes the org from the orgId reference
|
||||
|
||||
const project = await db(TableName.Project)
|
||||
.where({ [`${TableName.Project}.id` as "id"]: projectId })
|
||||
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Organization).as("organizationId"),
|
||||
db.ref("name").withSchema(TableName.Organization).as("organizationName")
|
||||
)
|
||||
.select(selectAllTableCols(TableName.Project))
|
||||
.first();
|
||||
|
||||
if (!project) {
|
||||
throw new BadRequestError({ message: "Project not found" });
|
||||
}
|
||||
|
||||
return {
|
||||
...ProjectsSchema.parse(project),
|
||||
organization: {
|
||||
id: project.organizationId,
|
||||
name: project.organizationName
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
...projectOrm,
|
||||
findAllProjects,
|
||||
@ -288,6 +316,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
findProjectById,
|
||||
findProjectByFilter,
|
||||
findProjectBySlug,
|
||||
findProjectWithOrg,
|
||||
checkProjectUpgradeStatus
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { ProjectVersion, TProjects } from "@app/db/schemas";
|
||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
|
||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||
};
|
||||
|
||||
export const verifyProjectVersions = (projects: Pick<TProjects, "version">[], version: ProjectVersion) => {
|
||||
for (const project of projects) {
|
||||
if (project.version !== version) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getProjectKmsCertificateKeyId = async ({
|
||||
projectId,
|
||||
projectDAL,
|
||||
|
@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
@ -30,6 +31,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
@ -44,6 +47,7 @@ import {
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
TListProjectCertsDTO,
|
||||
TListProjectsDTO,
|
||||
TLoadProjectKmsBackupDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
| "updateProjectSecretManagerKmsKey"
|
||||
@ -112,6 +117,7 @@ export const projectServiceFactory = ({
|
||||
projectEnvDAL,
|
||||
licenseService,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
@ -389,8 +395,34 @@ export const projectServiceFactory = ({
|
||||
return deletedProject;
|
||||
};
|
||||
|
||||
const getProjects = async (actorId: string) => {
|
||||
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId);
|
||||
|
||||
if (includeRoles) {
|
||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
||||
|
||||
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
const customRoles = await projectRoleDAL.find({
|
||||
$in: {
|
||||
projectId: workspaces.map((workspace) => workspace.id)
|
||||
}
|
||||
});
|
||||
|
||||
const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId);
|
||||
|
||||
const workspacesWithRoles = await Promise.all(
|
||||
workspaces.map(async (workspace) => {
|
||||
return {
|
||||
...workspace,
|
||||
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return workspacesWithRoles;
|
||||
}
|
||||
|
||||
return workspaces;
|
||||
};
|
||||
|
||||
|
@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
|
||||
actorOrgId: string | undefined;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListProjectsDTO = {
|
||||
includeRoles: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpgradeProjectDTO = {
|
||||
userPrivateKey: string;
|
||||
} & TProjectPermission;
|
||||
|
@ -1,3 +1,6 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TSecretSharing } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { SecretSharingAccessType } from "@app/lib/types";
|
||||
@ -36,6 +39,7 @@ export const secretSharingServiceFactory = ({
|
||||
iv,
|
||||
tag,
|
||||
name,
|
||||
password,
|
||||
accessType,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
@ -60,8 +64,10 @@ export const secretSharingServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
|
||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
name,
|
||||
password: hashedPassword,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
@ -77,6 +83,7 @@ export const secretSharingServiceFactory = ({
|
||||
};
|
||||
|
||||
const createPublicSharedSecret = async ({
|
||||
password,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
@ -102,7 +109,9 @@ export const secretSharingServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
|
||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
password: hashedPassword,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
@ -111,6 +120,7 @@ export const secretSharingServiceFactory = ({
|
||||
expiresAfterViews,
|
||||
accessType
|
||||
});
|
||||
|
||||
return { id: newSharedSecret.id };
|
||||
};
|
||||
|
||||
@ -152,7 +162,21 @@ export const secretSharingServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
|
||||
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
|
||||
const { expiresAfterViews } = sharedSecret;
|
||||
|
||||
if (expiresAfterViews) {
|
||||
// decrement view count if view count expiry set
|
||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||
}
|
||||
|
||||
await secretSharingDAL.updateById(sharedSecretId, {
|
||||
lastViewedAt: new Date()
|
||||
});
|
||||
};
|
||||
|
||||
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
|
||||
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
|
||||
const sharedSecret = await secretSharingDAL.findOne({
|
||||
id: sharedSecretId,
|
||||
hashedHex
|
||||
@ -169,6 +193,8 @@ export const secretSharingServiceFactory = ({
|
||||
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
|
||||
throw new UnauthorizedError();
|
||||
|
||||
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
|
||||
// or can be safely sent to the client.
|
||||
if (expiresAt !== null && expiresAt < new Date()) {
|
||||
// check lifetime expiry
|
||||
await secretSharingDAL.softDeleteById(sharedSecretId);
|
||||
@ -185,21 +211,29 @@ export const secretSharingServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (expiresAfterViews) {
|
||||
// decrement view count if view count expiry set
|
||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||
const isPasswordProtected = Boolean(sharedSecret.password);
|
||||
const hasProvidedPassword = Boolean(password);
|
||||
if (isPasswordProtected) {
|
||||
if (hasProvidedPassword) {
|
||||
const isMatch = await bcrypt.compare(password as string, sharedSecret.password as string);
|
||||
if (!isMatch) throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
} else {
|
||||
return { isPasswordProtected };
|
||||
}
|
||||
}
|
||||
|
||||
await secretSharingDAL.updateById(sharedSecretId, {
|
||||
lastViewedAt: new Date()
|
||||
});
|
||||
// decrement when we are sure the user will view secret.
|
||||
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
|
||||
|
||||
return {
|
||||
...sharedSecret,
|
||||
orgName:
|
||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||
? orgName
|
||||
: undefined
|
||||
isPasswordProtected,
|
||||
secret: {
|
||||
...sharedSecret,
|
||||
orgName:
|
||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||
? orgName
|
||||
: undefined
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -216,6 +250,6 @@ export const secretSharingServiceFactory = ({
|
||||
createPublicSharedSecret,
|
||||
getSharedSecrets,
|
||||
deleteSharedSecretById,
|
||||
getActiveSharedSecretById
|
||||
getSharedSecretById
|
||||
};
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ export type TSharedSecretPermission = {
|
||||
orgId: string;
|
||||
accessType?: SecretSharingAccessType;
|
||||
name?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export type TCreatePublicSharedSecretDTO = {
|
||||
@ -24,6 +25,7 @@ export type TCreatePublicSharedSecretDTO = {
|
||||
tag: string;
|
||||
expiresAt: string;
|
||||
expiresAfterViews?: number;
|
||||
password?: string;
|
||||
accessType: SecretSharingAccessType;
|
||||
};
|
||||
|
||||
@ -31,6 +33,11 @@ export type TGetActiveSharedSecretByIdDTO = {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string;
|
||||
orgId?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||
|
@ -25,6 +25,7 @@ export enum SmtpTemplates {
|
||||
UnlockAccount = "unlockAccount.handlebars",
|
||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
|
||||
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview.handlebars",
|
||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||
NewDeviceJoin = "newDevice.handlebars",
|
||||
OrgInvite = "organizationInvitation.handlebars",
|
||||
|
@ -9,7 +9,7 @@
|
||||
<body>
|
||||
<h2>Join your organization on Infisical</h2>
|
||||
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
|
||||
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
</body>
|
||||
|
@ -0,0 +1,22 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Secret Change Approval Request</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Hi {{firstName}},</h2>
|
||||
<h2>New secret change requests are pending review.</h2>
|
||||
<br />
|
||||
<p>You have a secret change request pending your review in project "{{projectName}}", in the "{{organizationName}}"
|
||||
organization.</p>
|
||||
|
||||
<p>
|
||||
View the request and approve or deny it
|
||||
<a href="{{approvalUrl}}">here</a>.
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -100,7 +100,9 @@ export type TIntegrationCreatedEvent = {
|
||||
export type TUserOrgInvitedEvent = {
|
||||
event: PostHogEventTypes.UserOrgInvitation;
|
||||
properties: {
|
||||
inviteeEmail: string;
|
||||
inviteeEmails: string[];
|
||||
projectIds?: string[];
|
||||
organizationRoleSlug?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -11,12 +11,25 @@ sinks:
|
||||
config:
|
||||
path: "access-token"
|
||||
templates:
|
||||
- source-path: my-dot-ev-secret-template
|
||||
- template-content: |
|
||||
{{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }}
|
||||
{{- range . }}
|
||||
{{ .Key }}={{ .Value }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
destination-path: my-dot-env-0.env
|
||||
config:
|
||||
polling-interval: 60s
|
||||
execute:
|
||||
command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
- base64-template-content: e3stIHdpdGggc2VjcmV0ICIyMDJmMDRkNy1lNGNiLTQzZDQtYTI5Mi1lODkzNzEyZDYxZmMiICJkZXYiICIvIiB9fQp7ey0gcmFuZ2UgLiB9fQp7eyAuS2V5IH19PXt7IC5WYWx1ZSB9fQp7ey0gZW5kIH19Cnt7LSBlbmQgfX0=
|
||||
destination-path: my-dot-env.env
|
||||
config:
|
||||
polling-interval: 60s
|
||||
execute:
|
||||
command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
- source-path: my-dot-ev-secret-template1
|
||||
destination-path: my-dot-env-1.env
|
||||
config:
|
||||
|
@ -95,6 +95,7 @@ type Template struct {
|
||||
SourcePath string `yaml:"source-path"`
|
||||
Base64TemplateContent string `yaml:"base64-template-content"`
|
||||
DestinationPath string `yaml:"destination-path"`
|
||||
TemplateContent string `yaml:"template-content"`
|
||||
|
||||
Config struct { // Configurations for the template
|
||||
PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret
|
||||
@ -432,6 +433,30 @@ func ProcessBase64Template(templateId int, encodedTemplate string, data interfac
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
func ProcessLiteralTemplate(templateId int, templateString string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
|
||||
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
|
||||
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
|
||||
funcs := template.FuncMap{
|
||||
"secret": secretFunction,
|
||||
"dynamic_secret": dynamicSecretFunction,
|
||||
}
|
||||
|
||||
templateName := "literalTemplate"
|
||||
|
||||
tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
|
||||
type AgentManager struct {
|
||||
accessToken string
|
||||
accessTokenTTL time.Duration
|
||||
@ -820,6 +845,8 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId
|
||||
|
||||
if secretTemplate.SourcePath != "" {
|
||||
processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
|
||||
} else if secretTemplate.TemplateContent != "" {
|
||||
processedTemplate, err = ProcessLiteralTemplate(templateId, secretTemplate.TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
|
||||
} else {
|
||||
processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -152,6 +153,28 @@ var loginCmd = &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if clearSelfHostedDomains {
|
||||
infisicalConfig, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
infisicalConfig.Domains = []string{}
|
||||
err = util.WriteConfigFile(&infisicalConfig)
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
fmt.Println("Cleared all self-hosted domains from the config file")
|
||||
return
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
@ -464,6 +487,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file")
|
||||
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
|
||||
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
|
||||
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
|
||||
@ -499,10 +523,12 @@ func DomainOverridePrompt() (bool, error) {
|
||||
}
|
||||
|
||||
func askForDomain() error {
|
||||
//query user to choose between Infisical cloud or self hosting
|
||||
|
||||
// query user to choose between Infisical cloud or self hosting
|
||||
const (
|
||||
INFISICAL_CLOUD = "Infisical Cloud"
|
||||
SELF_HOSTING = "Self Hosting"
|
||||
ADD_NEW_DOMAIN = "Add a new domain"
|
||||
)
|
||||
|
||||
options := []string{INFISICAL_CLOUD, SELF_HOSTING}
|
||||
@ -524,6 +550,36 @@ func askForDomain() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
infisicalConfig, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err)
|
||||
}
|
||||
|
||||
if infisicalConfig.Domains != nil && len(infisicalConfig.Domains) > 0 {
|
||||
// If domains are present in the config, let the user select from the list or select to add a new domain
|
||||
|
||||
items := append(infisicalConfig.Domains, ADD_NEW_DOMAIN)
|
||||
|
||||
prompt := promptui.Select{
|
||||
Label: "Which domain would you like to use?",
|
||||
Items: items,
|
||||
Size: 5,
|
||||
}
|
||||
|
||||
_, selectedOption, err := prompt.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if selectedOption != ADD_NEW_DOMAIN {
|
||||
config.INFISICAL_URL = fmt.Sprintf("%s/api", selectedOption)
|
||||
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", selectedOption)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
urlValidation := func(input string) error {
|
||||
_, err := url.ParseRequestURI(input)
|
||||
if err != nil {
|
||||
@ -542,12 +598,23 @@ func askForDomain() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//trimmed the '/' from the end of the self hosting url
|
||||
|
||||
// Trimmed the '/' from the end of the self hosting url, and set the api & login url
|
||||
domain = strings.TrimRight(domain, "/")
|
||||
//set api and login url
|
||||
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
|
||||
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain)
|
||||
//return nil
|
||||
|
||||
// Write the new domain to the config file, to allow the user to select it in the future if needed
|
||||
// First check if infiscialConfig.Domains already includes the domain, if it does, do not add it again
|
||||
if !slices.Contains(infisicalConfig.Domains, domain) {
|
||||
infisicalConfig.Domains = append(infisicalConfig.Domains, domain)
|
||||
err = util.WriteConfigFile(&infisicalConfig)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ type ConfigFile struct {
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
type LoggedInUser struct {
|
||||
|
@ -19,10 +19,11 @@ Every new joiner has an onboarding buddy who should ideally be in the the same t
|
||||
1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT.
|
||||
2. Ship something together on day one – even if tiny! It feels great to hit the ground running, with a development environment all ready to go.
|
||||
3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into.
|
||||
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1oy_NP1Q_Zt1oqxLpyNkLIGmhAI3N28AmZq6dDIOONSQ/edit?usp=sharing).
|
||||
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1RaJd3RoS2QpWLFHlgfHaXnHqCCwRt6mCGZkbJ75J_D0/edit?usp=sharing).
|
||||
5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts.
|
||||
6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better.
|
||||
7. Change your Slack username in the users channel to `[NAME] (Infisical)`.
|
||||
8. Go through the [technical overview](https://infisical.com/docs/internals/overview) of Infisical.
|
||||
9. Request a company credit card (Maidul will be able to help with that).
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
volumes:
|
||||
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
|
@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "Retrieve CRL"
|
||||
openapi: "GET /api/v1/pki/ca/{caId}/crl"
|
||||
title: "List CRLs"
|
||||
openapi: "GET /api/v1/pki/ca/{caId}/crls"
|
||||
---
|
||||
|
144
docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx
Normal file
144
docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx
Normal file
@ -0,0 +1,144 @@
|
||||
---
|
||||
title: "AWS Elasticahe"
|
||||
description: "Learn how to dynamically generate Redis Database user credentials."
|
||||
---
|
||||
|
||||
The Infisical Redis dynamic secret allows you to generate Redis Database credentials on demand based on configured role.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
|
||||
2. Create an AWS IAM user with the following permissions:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"elasticache:DescribeUsers",
|
||||
"elasticache:ModifyUser",
|
||||
"elasticache:CreateUser",
|
||||
"elasticache:CreateUserGroup",
|
||||
"elasticache:DeleteUser",
|
||||
"elasticache:DescribeReplicationGroups",
|
||||
"elasticache:DescribeUserGroups",
|
||||
"elasticache:ModifyReplicationGroup",
|
||||
"elasticache:ModifyUserGroup"
|
||||
],
|
||||
"Resource": "arn:aws:elasticache:<region>:<account-id>:user:*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. Create an access key ID and secret access key for the user you created in the previous step. You will need these to configure the Infisical dynamic secret.
|
||||
|
||||
<Note>
|
||||
New leases may take up-to a couple of minutes before ElastiCache has the chance to complete their configuration.
|
||||
It is recommended to use a retry strategy when establishing new Redis ElastiCache connections.
|
||||
This may prevent errors when trying to use a password that isn't yet live on the targeted ElastiCache cluster.
|
||||
|
||||
While a leasing is being created, you will be unable to create new leases for the same dynamic secret.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Please ensure that your ElastiCache cluster has transit encryption enabled and set to required. This is required for the dynamic secret to work.
|
||||
</Note>
|
||||
|
||||
|
||||
|
||||
|
||||
## Set up Dynamic Secrets with Redis
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Overview Dashboard">
|
||||
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select 'Redis'">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Region" type="string" required>
|
||||
The region that the ElastiCache cluster is located in. _(e.g. us-east-1)_
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Access Key ID" type="string" required>
|
||||
This is the access key ID of the AWS IAM user you created in the prerequisites. This will be used to provision and manage the dynamic secret leases.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Secret Access Key" type="string" required>
|
||||
This is the secret access key of the AWS IAM user you created in the prerequisites. This will be used to provision and manage the dynamic secret leases.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="CA(SSL)" type="string">
|
||||
A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service.
|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
<Step title="(Optional) Modify ElastiCache Statements">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the ElastiCache statement to your needs. This is useful if you want to only give access to a specific table(s).
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Click `Submit`">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
|
||||
<Note>
|
||||
If this step fails, you may have to add the CA certificate.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
|
||||

|
||||

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

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

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

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

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||
</Warning>
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "AWS IAM"
|
||||
description: "How to dynamically generate AWS IAM Users."
|
||||
description: "Learn how to dynamically generate AWS IAM Users."
|
||||
---
|
||||
|
||||
The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users on demand based on configured AWS policy.
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Cassandra"
|
||||
description: "How to dynamically generate Cassandra database users."
|
||||
description: "Learn how to dynamically generate Cassandra database user credentials"
|
||||
---
|
||||
|
||||
The Infisical Cassandra dynamic secret allows you to generate Cassandra database credentials on demand based on configured role.
|
||||
|
114
docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx
Normal file
114
docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
title: "Mongo Atlas"
|
||||
description: "Learn how to dynamically generate Mongo Atlas Database user credentials."
|
||||
---
|
||||
|
||||
The Infisical Mongo Atlas dynamic secret allows you to generate Mongo Atlas Database credentials on demand based on configured role.
|
||||
|
||||
## Prerequisite
|
||||
Create a project scopped API Key with the required permission in your Mongo Atlas following the [official doc](https://www.mongodb.com/docs/atlas/configure-api-access/#grant-programmatic-access-to-a-project).
|
||||
|
||||
<Info>
|
||||
The API Key must have permission to manage users in the project.
|
||||
</Info>
|
||||
|
||||
## Set up Dynamic Secrets with Mongo Atlas
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Overview Dashboard">
|
||||
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select Mongo Atlas">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Admin public key" type="string" required>
|
||||
The public key of your generated Atlas API Key. This acts as a username.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Admin private key" type="string" required>
|
||||
The private key of your generated Atlas API Key. This acts as a password.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Group ID" type="number" required>
|
||||
Unique 24-hexadecimal digit string that identifies your project. This is same as project id
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Roles" type="string" required>
|
||||
List that provides the pairings of one role with one applicable database.
|
||||
- **Database Name**: Database to which the user is granted access privileges.
|
||||
- **Collection**: Collection on which this role applies.
|
||||
- **Role Name**: Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
- Enum: `atlasAdmin` `backup` `clusterMonitor` `dbAdmin` `dbAdminAnyDatabase` `enableSharding` `read` `readAnyDatabase` `readWrite` `readWriteAnyDatabase` `<a custom role name>`.
|
||||
</ParamField>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="(Optional) Modify Access Scope">
|
||||
List that contains clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances that this database user can access. If omitted, MongoDB Cloud grants the database user access to all the clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances in the project.
|
||||
|
||||

|
||||
- **Label**: Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access.
|
||||
- **Type**: Category of resource that this database user can access.
|
||||
</Step>
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
|
||||
<Note>
|
||||
If this step fails, you may have to add the CA certficate.
|
||||
</Note>
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
|
||||

|
||||

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

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

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

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

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||
</Warning>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user