Compare commits

...

134 Commits

Author SHA1 Message Date
Maidul Islam
d0caef37ce Merge pull request #2349 from Infisical/mzidul-wjdhbwhufhjwebf
Add tool tip for k8s auth
2024-08-28 17:11:06 -04:00
Maidul Islam
2d26febe58 a to an 2024-08-28 17:09:47 -04:00
Maidul Islam
c23ad8ebf2 improve tooltip 2024-08-28 17:04:56 -04:00
Maidul Islam
bad068ef19 add tool tip for k8s auth 2024-08-28 16:59:14 -04:00
Daniel Hougaard
53430608a8 Merge pull request #2348 from Infisical/daniel/env-transform-trailing-slashes
Fix: Always remove trailing slashes from SITE_URL
2024-08-28 22:52:03 +04:00
Daniel Hougaard
b9071ab2b3 Fix: Always remove trailing slashes from SITE_URL 2024-08-28 22:45:08 +04:00
Daniel Hougaard
bfab270d68 Merge pull request #2347 from Infisical/daniel/fix-secret-change-emails
Fix: Removed protocol parsing on secret change emails
2024-08-28 22:16:28 +04:00
Daniel Hougaard
8ea6a1f3d5 Fix: Removed protocol parsing 2024-08-28 22:07:31 +04:00
Daniel Hougaard
828644799f Merge pull request #2319 from Infisical/daniel/redis-dynamic-secrets
Feat: Redis support for dynamic secrets
2024-08-28 18:06:57 +04:00
Daniel Hougaard
411e67ae41 Finally resolved package-lock 2024-08-28 18:01:31 +04:00
Daniel Hougaard
4914bc4b5a Fix: Package json bugged generation 2024-08-28 18:00:05 +04:00
Daniel Hougaard
d7050a1947 Update package-lock.json 2024-08-28 17:58:32 +04:00
Daniel Hougaard
3c59422511 Fixed package 2024-08-28 17:57:31 +04:00
Daniel Hougaard
c81204e6d5 Test 2024-08-28 17:57:31 +04:00
Daniel Hougaard
880f39519f Update aws-elasticache.mdx 2024-08-28 17:57:31 +04:00
Daniel Hougaard
8646f6c50b Requested changes 2024-08-28 17:57:30 +04:00
Daniel Hougaard
437a9e6ccb AWS elasticache 2024-08-28 17:57:30 +04:00
Daniel Hougaard
b54139bd37 Fix 2024-08-28 17:57:30 +04:00
Daniel Hougaard
8a6a36ac54 Update package-lock.json 2024-08-28 17:57:20 +04:00
Daniel Hougaard
c6eb973da0 Uninstalled unused dependencies 2024-08-28 17:57:19 +04:00
Daniel Hougaard
21750a8c20 Fix: Refactored aws elasticache to separate provider 2024-08-28 17:57:19 +04:00
Daniel Hougaard
a598665b2f Docs: ElastiCache Docs 2024-08-28 17:57:19 +04:00
Daniel Hougaard
56bbf502a2 Update redis.ts 2024-08-28 17:57:19 +04:00
Daniel Hougaard
9975f7d83f Edition fixes 2024-08-28 17:57:19 +04:00
Daniel Hougaard
7ad366b363 Update licence-fns.ts 2024-08-28 17:57:19 +04:00
Daniel Hougaard
cca4d68d94 Fix: AWS ElastiCache support 2024-08-28 17:57:19 +04:00
Daniel Hougaard
b82b94db54 Docs: Redis Dynamic secrets docs 2024-08-28 17:55:04 +04:00
Daniel Hougaard
de9cb265e0 Feat: Redis support for dynamic secrets 2024-08-28 17:55:04 +04:00
Maidul Islam
7ac4ad3194 Merge pull request #2344 from Infisical/maidul-ddqdqwdqwd3
Update health check
2024-08-27 20:09:51 -04:00
Maidul Islam
3ab6eb62c8 update health check 2024-08-27 20:03:36 -04:00
Sheen
79680b6a73 Merge pull request #2340 from Infisical/misc/added-timeout-for-hijacked-est-connection
misc: added timeout for est connection
2024-08-27 23:31:07 +08:00
Sheen Capadngan
58838c541f misc: added timeout for est connection 2024-08-27 23:26:56 +08:00
Sheen
03cc71cfed Merge pull request #2284 from Infisical/feature/est-simpleenroll
Certificate EST protocol (simpleenroll, simplereenroll, cacerts)
2024-08-27 13:42:58 +08:00
Maidul Islam
02529106c9 Merge pull request #2336 from akhilmhdh/fix/scim-error
fix: resolved scim group update failing
2024-08-26 16:34:27 -04:00
=
d939ff289d fix: resolved scim group update failing 2024-08-27 01:50:26 +05:30
Daniel Hougaard
d1816c3051 Merge pull request #2334 from Infisical/daniel/azure-devops-docs
Docs: Azure DevOps Integration
2024-08-26 23:49:23 +04:00
Daniel Hougaard
cb350788c0 Update create.tsx 2024-08-26 23:21:56 +04:00
Daniel Hougaard
cd58768d6f Updated images 2024-08-26 23:20:51 +04:00
Daniel Hougaard
dcd6f4d55d Fix: Updated Azure DevOps integration styling 2024-08-26 23:12:00 +04:00
Daniel Hougaard
3c828614b8 Fix: Azure DevOps Label naming typos 2024-08-26 22:44:11 +04:00
Daniel Hougaard
09e7988596 Docs: Azure DevOps Integration 2024-08-26 22:43:49 +04:00
Sheen Capadngan
f40df19334 misc: finalized est config schema 2024-08-27 02:15:01 +08:00
Sheen Capadngan
76c9d3488b Merge remote-tracking branch 'origin/main' into feature/est-simpleenroll 2024-08-27 02:13:59 +08:00
Sheen Capadngan
0809da33e0 misc: improved docs and added support for curl clients 2024-08-27 02:05:35 +08:00
Daniel Hougaard
b528eec4bb Merge pull request #2333 from Infisical/daniel/secret-change-emails
Feat: Email notification on secret change requests
2024-08-26 21:50:30 +04:00
Daniel Hougaard
5179103680 Update SecretApprovalRequest.tsx 2024-08-26 21:46:05 +04:00
Daniel Hougaard
25a9e5f58a Update SecretApprovalRequest.tsx 2024-08-26 21:42:47 +04:00
Daniel Hougaard
8ddfe7b6e9 Update secret-approval-request-fns.ts 2024-08-26 20:53:43 +04:00
Daniel Hougaard
c23f21d57a Update SecretApprovalRequest.tsx 2024-08-26 20:21:05 +04:00
Daniel Hougaard
1242a43d98 Feat: Open approval with ID in URL 2024-08-26 20:04:06 +04:00
Daniel Hougaard
1655ca27d1 Fix: Creation of secret approval policies 2024-08-26 20:02:58 +04:00
Daniel Hougaard
2bcead03b0 Feat: Send secret change request emails to approvers 2024-08-26 19:55:04 +04:00
Daniel Hougaard
41ab1972ce Feat: Find project and include org dal 2024-08-26 19:54:48 +04:00
Daniel Hougaard
b00fff6922 Update index.ts 2024-08-26 19:54:15 +04:00
Daniel Hougaard
97b01ca5f8 Feat: Send secret change request emails to approvers 2024-08-26 19:54:01 +04:00
Daniel Hougaard
c2bd6f5ef3 Feat: Send secret change request emails to approvers 2024-08-26 19:53:49 +04:00
Daniel Hougaard
18efc9a6de Include more user details 2024-08-26 19:53:17 +04:00
Daniel Hougaard
436ccb25fb Merge pull request #2331 from Infisical/daniel/presist-selfhosting-domains
Feat: Persistent self-hosting domains on `infisical login`
2024-08-26 18:04:25 +04:00
Daniel Hougaard
8f08a352dd Merge pull request #2310 from Infisical/daniel/azure-devops-integration
Feat: Azure DevOps Integration
2024-08-26 18:04:04 +04:00
Sheen Capadngan
00f86cfd00 misc: addressed review comments 2024-08-26 21:10:29 +08:00
Daniel Hougaard
3944aafb11 Use slices 2024-08-26 15:18:45 +04:00
Daniel Hougaard
a6b852fab9 Fix: Type errors / cleanup 2024-08-26 15:13:18 +04:00
Daniel Hougaard
2a043afe11 Cleanup 2024-08-26 15:13:18 +04:00
Daniel Hougaard
df8f2cf9ab Update integration-sync-secret.ts 2024-08-26 15:13:18 +04:00
Daniel Hougaard
a18015b1e5 Fix: Use unique parameter for passing devops org name
Used to be teamId, now it's azureDevopsOrgName.
2024-08-26 15:13:18 +04:00
Daniel Hougaard
8b80622d2f Cleanup 2024-08-26 15:13:18 +04:00
Daniel Hougaard
c0fd0a56f3 Update integration-list.ts 2024-08-26 15:13:18 +04:00
Daniel Hougaard
326764dd41 Feat: Azure DevOps Integration 2024-08-26 15:13:18 +04:00
Daniel Hougaard
1f24d02c5e Fix: Do not save duplicate domains 2024-08-26 15:08:51 +04:00
Daniel Hougaard
c130fbddd9 Merge pull request #2321 from Infisical/daniel/specify-roles-and-projects
Feat: Select roles & projects when inviting members to organization
2024-08-26 15:00:39 +04:00
Tuan Dang
f560534493 Replace custom pkcs7 fns with module 2024-08-25 20:21:53 -07:00
Maidul Islam
10a97f4522 update python docs to point to new repo 2024-08-25 18:02:51 -04:00
Daniel Hougaard
7a2f0214f3 Feat: Persist self-hosting domains on infisical login 2024-08-24 13:18:03 +04:00
Daniel Hougaard
a2b994ab23 Requested changes 2024-08-24 11:00:05 +04:00
Maidul Islam
c4715124dc Merge pull request #2327 from Infisical/fix/resolve-name-null-null
fix: this pr addresses null null name issue with invited users
2024-08-23 14:01:27 -04:00
BlackMagiq
68b1984a76 Merge pull request #2325 from Infisical/crl-update
CRL Distribution Point URLs + Support for Multiple CRLs per CA
2024-08-22 23:56:55 -07:00
Tuan Dang
ba45e83880 Clean 2024-08-22 23:37:36 -07:00
Daniel Hougaard
28ecc37163 Update org-service.ts 2024-08-23 03:10:14 +04:00
Daniel Hougaard
a6a2e2bae0 Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
Daniel Hougaard
d8bbfacae0 UI improvements 2024-08-23 02:25:15 +04:00
Daniel Hougaard
58549c398f Update project-service.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
842ed62bec Rename 2024-08-23 02:25:15 +04:00
Daniel Hougaard
06d8800ee0 Feat: Specify organization role and projects when inviting users to org 2024-08-23 02:25:15 +04:00
Daniel Hougaard
2ecfd1bb7e Update auth-signup-type.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
783d4c7bd6 Update org-dal.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
fbf3f26abd Refactored org invites to allow for multiple users and to handle project invites 2024-08-23 02:25:15 +04:00
Daniel Hougaard
1d09693041 Update org-types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
626e37e3d0 Moved project membership creation to project membership fns 2024-08-23 02:25:15 +04:00
Daniel Hougaard
07fd67b328 Add metadata to SMTP email 2024-08-23 02:25:15 +04:00
Daniel Hougaard
3f1f018adc Update telemetry-types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
fe04e6d20c Remove *.*.posthog.com 2024-08-23 02:25:15 +04:00
Daniel Hougaard
d7171a1617 Removed unused code 2024-08-23 02:25:15 +04:00
Daniel Hougaard
384a0daa31 Update types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
c5c949e034 Multi user org invites 2024-08-23 02:25:15 +04:00
Daniel Hougaard
c2c9edf156 Update types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
c8248ef4e9 Fix: Skip org selection when user only has one org 2024-08-23 02:25:15 +04:00
Daniel Hougaard
9f6a6a7b7c Automatic timed toggle 2024-08-23 02:25:15 +04:00
Daniel Hougaard
121b642d50 Added new metadata parameter for signup 2024-08-23 02:25:15 +04:00
Daniel Hougaard
59b16f647e Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
Daniel Hougaard
2ab5932693 Update OrgMembersSection.tsx 2024-08-23 02:25:15 +04:00
Daniel Hougaard
8dfcef3900 Seperate component for Org Invite Links 2024-08-23 02:25:15 +04:00
Daniel Hougaard
8ca70eec44 Refactor add users to org handlers 2024-08-23 02:25:14 +04:00
Daniel Hougaard
60df59c7f0 Multi-user organization invites structure 2024-08-23 02:25:14 +04:00
Daniel Hougaard
e231c531a6 Update index.ts 2024-08-23 02:25:14 +04:00
Daniel Hougaard
d48bb910fa JWT invite lifetime (1 day) 2024-08-23 02:25:14 +04:00
Tuan Dang
1317266415 Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-22 14:54:34 -07:00
Tuan Dang
288f47f4bd Update API reference CRL docs 2024-08-22 12:30:48 -07:00
Tuan Dang
b090ebfd41 Update API reference CRL docs 2024-08-22 12:26:13 -07:00
Tuan Dang
67773bff5e Update wording on external parent ca 2024-08-22 12:18:00 -07:00
Tuan Dang
8ef1cfda04 Update docs for CRL 2024-08-22 12:16:37 -07:00
Tuan Dang
2a79d5ba36 Fix merge conflicts 2024-08-22 12:01:43 -07:00
Tuan Dang
0cb95f36ff Finish updating CRL impl 2024-08-22 11:55:19 -07:00
Sheen Capadngan
288d7e88ae misc: made SSL header key configurable via env 2024-08-23 01:38:12 +08:00
Sheen Capadngan
f88389bf9e misc: added general format 2024-08-22 21:05:34 +08:00
Sheen Capadngan
2e88c5e2c5 misc: improved url examples in est doc 2024-08-22 21:02:38 +08:00
Sheen Capadngan
73f3b8173e doc: added guide for EST usage' 2024-08-22 20:44:21 +08:00
Sheen Capadngan
aa5b88ff04 misc: removed enrollment options from CA page 2024-08-22 15:40:36 +08:00
Sheen Capadngan
b7caff88cf feat: finished up EST cacerts 2024-08-22 15:39:53 +08:00
Sheen Capadngan
760a1e917a feat: added simplereenroll 2024-08-20 23:56:27 +08:00
Sheen Capadngan
2d7ff66246 Merge branch 'feature/est-simpleenroll' of https://github.com/Infisical/infisical into feature/est-simpleenroll 2024-08-20 15:31:58 +08:00
Sheen Capadngan
179497e830 misc: moved est logic to service 2024-08-20 15:31:10 +08:00
Tuan Dang
4c08c80e5b Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-19 14:53:04 -07:00
Sheen Capadngan
7d6af64904 misc: added proxy header for amazon mtls client cert 2024-08-20 01:53:47 +08:00
Sheen Capadngan
16519f9486 feat: added reading SANs from CSR 2024-08-20 01:39:40 +08:00
Sheen Capadngan
bb27d38a12 misc: ui form adjustments 2024-08-19 21:39:00 +08:00
Sheen Capadngan
5b26928751 misc: added audit logs 2024-08-19 20:25:07 +08:00
Sheen Capadngan
f425e7e48f misc: addressed alignment issue 2024-08-19 19:50:09 +08:00
Sheen Capadngan
4601f46afb misc: finalized variable naming 2024-08-19 19:33:46 +08:00
Sheen Capadngan
692bdc060c misc: updated est configuration to be binded to certificate template 2024-08-19 19:26:20 +08:00
Sheen Capadngan
3a4f8c2e54 Merge branch 'feature/certificate-template' into feature/est-simpleenroll 2024-08-19 17:04:22 +08:00
Sheen Capadngan
146c4284a2 feat: integrated to est routes 2024-08-14 20:52:21 +08:00
Sheen Capadngan
5ae33b9f3b misc: minor UI updates 2024-08-14 01:10:25 +08:00
Sheen Capadngan
1f38b92ec6 feat: finished up integration for est config management 2024-08-14 01:00:31 +08:00
Sheen Capadngan
f2a49a79f0 feat: initial simpleenroll setup (mvp) 2024-08-13 23:22:47 +08:00
175 changed files with 6525 additions and 1015 deletions

View File

@@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=

View File

@@ -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,6 +75,7 @@
"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",
@@ -351,6 +353,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 +4762,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 +5345,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 +8797,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",
@@ -14120,6 +14444,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",
@@ -16268,9 +16608,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",

View File

@@ -106,6 +106,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 +127,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,6 +172,7 @@
"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",

View File

@@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
@@ -160,6 +161,7 @@ declare module "fastify" {
certificateTemplate: TCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory;
pkiCollection: TPkiCollectionServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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");
});
}
}

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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>;

View 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>
>;

View File

@@ -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";

View File

@@ -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",

View File

@@ -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(),

View File

@@ -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"
// };
// }
// });
};

View File

@@ -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" }
);

View File

@@ -9,7 +9,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
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) {
@@ -474,18 +477,18 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
Operations: z.array(
z.union([
z.object({
op: z.literal("replace"),
op: z.union([z.literal("replace"), z.literal("Replace")]),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({
op: z.literal("remove"),
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim()
}),
z.object({
op: z.literal("add"),
op: z.union([z.literal("add"), z.literal("Add")]),
path: z.string().trim(),
value: z.array(
z.object({

View File

@@ -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;

View File

@@ -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
};
};

View File

@@ -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">;

View File

@@ -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,

View File

@@ -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
};
};

View File

@@ -1,10 +1,14 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { CassandraProvider } from "./cassandra";
import { DynamicSecretProviders } from "./models";
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()
});

View File

@@ -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(),
@@ -47,13 +70,17 @@ export const DynamicSecretAwsIamSchema = z.object({
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
AwsIam = "aws-iam"
AwsIam = "aws-iam",
Redis = "redis",
AwsElastiCache = "aws-elasticache"
}
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 })
]);
export type TDynamicProviderFns = {

View 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
};
};

View File

@@ -50,8 +50,8 @@ export const buildScimUser = ({
orgMembershipId: string;
username: string;
email?: string | null;
firstName: string;
lastName: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
groups?: {
value: string;
display: string;
@@ -64,9 +64,9 @@ export const buildScimUser = ({
userName: username,
displayName: `${firstName} ${lastName}`,
name: {
givenName: firstName,
givenName: firstName || "",
middleName: null,
familyName: lastName
familyName: lastName || ""
},
emails: email
? [

View File

@@ -267,8 +267,8 @@ export const scimServiceFactory = ({
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,
@@ -427,8 +427,8 @@ 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
});
@@ -483,8 +483,8 @@ export const scimServiceFactory = ({
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
});
};
@@ -527,8 +527,8 @@ export const scimServiceFactory = ({
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,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
@@ -884,59 +884,50 @@ export const scimServiceFactory = ({
}
for await (const operation of operations) {
switch (operation.op) {
case "replace": {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
if (operation.op === "replace" || operation.op === "Replace") {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
});
} else if (operation.op === "add" || operation.op === "Add") {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
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({
await addUsersToGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL
});
break;
}
default: {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
} else if (operation.op === "remove" || operation.op === "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
});
} else {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
}
}

View File

@@ -110,8 +110,10 @@ export type TUpdateScimGroupNamePatchDTO = {
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
};
// akhilmhdh: I know, this is done due to lack of time. Need to change later to support as normalized rather than like this
// Forgive akhil blame tony
type TReplaceOp = {
op: "replace";
op: "replace" | "Replace";
value: {
id: string;
displayName: string;
@@ -119,12 +121,12 @@ type TReplaceOp = {
};
type TRemoveOp = {
op: "remove";
op: "remove" | "Remove";
path: string;
};
type TAddOp = {
op: "add";
op: "add" | "Add";
path: string;
value: {
value: string;

View File

@@ -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
})
}
]

View File

@@ -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
});
}
};

View File

@@ -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;
};

View File

@@ -1120,9 +1120,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 +1175,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",

View File

@@ -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,

View File

@@ -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) {

View 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
});
}
});
};

View File

@@ -1,4 +1,5 @@
import { CronJob } from "cron";
import { Redis } from "ioredis";
import { Knex } from "knex";
import { z } from "zod";
@@ -71,6 +72,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";
@@ -90,7 +92,9 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-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";
@@ -195,6 +199,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerCertificateEstRouter } from "./est/certificate-est-router";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
@@ -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,27 @@ export const registerRoutes = async (
certificateAuthorityCrlDAL,
projectDAL,
kmsService,
permissionService,
licenseService
permissionService
// licenseService
});
const certificateTemplateService = certificateTemplateServiceFactory({
certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL,
permissionService
permissionService,
kmsService,
projectDAL
});
const certificateEstService = certificateEstServiceFactory({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const pkiAlertService = pkiAlertServiceFactory({
@@ -683,6 +707,7 @@ export const registerRoutes = async (
orgDAL,
orgService,
projectMembershipDAL,
projectRoleDAL,
folderDAL,
licenseService,
certificateAuthorityDAL,
@@ -839,6 +864,7 @@ export const registerRoutes = async (
secretQueueService,
kmsService,
secretV2BridgeDAL,
secretApprovalPolicyDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
smtpService,
@@ -1189,6 +1215,7 @@ export const registerRoutes = async (
certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService,
pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService,
secretScanning: secretScanningService,
@@ -1232,7 +1259,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 +1268,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 +1308,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) => {

View File

@@ -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"
// };
// }
// });
};

View File

@@ -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

View File

@@ -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;
}
});
};

View File

@@ -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: {

View File

@@ -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(", ")}`
};
}
});

View File

@@ -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 };
}
});

View File

@@ -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({

View File

@@ -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;
};

View File

@@ -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 },

View File

@@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
ip: string;
userAgent: string;
authorization: string;
tokenMetadata?: string;
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -0,0 +1,231 @@
import * as x509 from "@peculiar/x509";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
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">;
};
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
export const certificateEstServiceFactory = ({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
}: TCertificateEstServiceFactoryDep) => {
const simpleReenroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
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
});
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 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
};
};

View File

@@ -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;
};

View File

@@ -1,20 +1,35 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import { TCertificateTemplateEstConfigsUpdate } 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 { 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">;
};
@@ -23,8 +38,11 @@ export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTe
export const certificateTemplateServiceFactory = ({
certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL,
permissionService
permissionService,
kmsService,
projectDAL
}: TCertificateTemplateServiceFactoryDep) => {
const createCertTemplate = async ({
caId,
@@ -187,10 +205,228 @@ export const certificateTemplateServiceFactory = ({
return certTemplate;
};
const createEstConfiguration = async ({
certificateTemplateId,
caChain,
passphrase,
isEnabled,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateEstConfigurationDTO) => {
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 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
};
};
return {
createCertTemplate,
getCertTemplate,
deleteCertTemplate,
updateCertTemplate
updateCertTemplate,
createEstConfiguration,
updateEstConfiguration,
getEstConfiguration
};
};

View File

@@ -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">);

View File

@@ -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;
};

View File

@@ -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" });
}

View File

@@ -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
});

View File

@@ -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;
};

View File

@@ -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: ""
}
];

View File

@@ -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,
@@ -2075,6 +2076,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 +3825,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,

View File

@@ -114,10 +114,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 +127,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);

View File

@@ -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;
}
};

View File

@@ -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 = {

View File

@@ -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
};
};

View File

@@ -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;
};

View 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));
};

View File

@@ -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,

View File

@@ -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
};
};

View File

@@ -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,

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -100,7 +100,9 @@ export type TIntegrationCreatedEvent = {
export type TUserOrgInvitedEvent = {
event: PostHogEventTypes.UserOrgInvitation;
properties: {
inviteeEmail: string;
inviteeEmails: string[];
projectIds?: string[];
organizationRoleSlug?: string;
};
};

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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"
---

View 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">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
</Step>
<Step title="Select 'Redis'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache)
</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).
![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png)
</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.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
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.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png)
<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.
![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png)
</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.
![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png)
## 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.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,6 +1,6 @@
---
title: "MS SQL"
description: "How to dynamically generate MS SQL database users."
description: "Learn how to dynamically generate MS SQL database user credentials."
---
The Infisical MS SQL dynamic secret allows you to generate Microsoft SQL server database credentials on demand based on configured role.

View File

@@ -1,6 +1,6 @@
---
title: "MySQL"
description: "Learn how to dynamically generate MySQL Database user passwords."
description: "Learn how to dynamically generate MySQL Database user credentials."
---
The Infisical MySQL dynamic secret allows you to generate MySQL Database credentials on demand based on configured role.

View File

@@ -1,6 +1,6 @@
---
title: "Oracle"
description: "Learn how to dynamically generate Oracle Database user passwords."
description: "Learn how to dynamically generate Oracle Database user credentials."
---
The Infisical Oracle dynamic secret allows you to generate Oracle Database credentials on demand based on configured role.

View File

@@ -32,4 +32,5 @@ Dynamic secrets are particularly useful in environments with stringent security
2. [MySQL](./mysql)
3. [Cassandra](./cassandra)
4. [Oracle](./oracle)
6. [Redis](./redis)
5. [AWS IAM](./aws-iam)

View File

@@ -1,6 +1,6 @@
---
title: "PostgreSQL"
description: "How to dynamically generate PostgreSQL database users."
description: "Learn how to dynamically generate PostgreSQL database users."
---
The Infisical PostgreSQL dynamic secret allows you to generate PostgreSQL database credentials on demand based on configured role.

View File

@@ -0,0 +1,106 @@
---
title: "Redis"
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.
## Prerequisite
Create a user with the required permission in your Redis instance. This user will be used to create new accounts on-demand.
## 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">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
</Step>
<Step title="Select 'Redis'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-redis.png)
</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="Host" type="string" required>
The database host, this can be an IP address or a domain name as long as Infisical can reach it.
</ParamField>
<ParamField path="Port" type="number" required>
The database port, this is the port that the Redis instance is listening on.
</ParamField>
<ParamField path="User" type="string" required>
Redis username that will be used to create new users on-demand. This is often 'default' or 'admin'.
</ParamField>
<ParamField path="Password" type="string" optional>
Password that will be used to create dynamic secrets. This is required if your Redis instance is password protected.
</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 Redis Statements">
If you want to provide specific privileges for the generated dynamic credentials, you can modify the Redis statement to your needs. This is useful if you want to only give access to a specific table(s).
![Modify Redis Statements Modal](/images/platform/dynamic-secrets/modify-redis-statement.png)
</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.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
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.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png)
<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.
![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png)
</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.
![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png)
## 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.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>

View File

@@ -151,18 +151,24 @@ In the following steps, we explore how to revoke a X.509 certificate under a CA
</Step>
<Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it
against the CRL of a CA by selecting the **View CRL** option under the
issuing CA and downloading the CRL file.
against the CRL of a CA by heading to its Issuing CA and downloading the CRL.
![pki view crl](/images/platform/pki/ca-crl.png)
![pki download crl](/images/platform/pki/ca-crl-modal.png)
To verify a certificate against the
downloaded CRL with OpenSSL, you can use the following command:
```bash
openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem
```
Note that you can also obtain the CRL from the certificate itself by
referencing the CRL distribution point extension on the certificate itself.
To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command:
```bash
openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem
```
</Step>
@@ -197,21 +203,25 @@ openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem
</Step>
<Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA.
To obtain the CRL of the CA, make an API request to the [Get CRL](/api-reference/endpoints/certificate-authorities/crl) API endpoint.
To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crls) API endpoint.
### Sample request
```bash Request
curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/crl' \
curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/crls' \
--header 'Authorization: Bearer <access-token>'
```
### Sample response
```bash Response
{
crl: "..."
}
[
{
id: "...",
crl: "..."
},
...
]
```
To verify a certificate against the CRL with OpenSSL, you can use the following command:

View File

@@ -0,0 +1,57 @@
---
title: "Enrollment over Secure Transport (EST)"
sidebarTitle: "Enrollment over Secure Transport (EST)"
description: "Learn how to manage certificate enrollment of clients using EST"
---
## Concept
Enrollment over Secure Transport (EST) is a protocol used to automate the secure provisioning of digital certificates for devices and applications over a secure HTTPS connection. It is primarily used when a client device needs to obtain or renew a certificate from a Certificate Authority (CA) on Infisical in a secure and standardized manner. EST is commonly employed in environments requiring strong authentication and encrypted communication, such as in IoT, enterprise networks, and secure web services.
Infisical's EST service is based on [RFC 7030](https://datatracker.ietf.org/doc/html/rfc7030) and implements the following endpoints:
- **cacerts** - provides the necessary CA chain for the client to validate certificates issued by the CA.
- **simpleenroll** - allows an EST client to request a new certificate from Infisical's EST server
- **simplereenroll** - similar to the /simpleenroll endpoint but is used for renewing an existing certificate.
These endpoints are exposed on port 8443 under the .well-known/est path e.g.
`https://app.infisical.com:8443/.well-known/est/estLabel/cacerts`
## Prerequisites
- You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca).
- The client devices need to have a bootstrap/pre-installed certificate.
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS.
## Guide to configuring EST
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates).
2. Proceed to the certificate template's enrollment settings
![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png)
3. Select **EST** as the client enrollment method and fill up the remaining fields.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png)
- **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server.
- **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password.
For security reasons, Infisical authenticates EST clients using both client certificate and passphrase.
4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png)
For demonstration, the complete URL of the supported EST endpoints will look like the following:
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simplereenroll
## Setting up EST clients
- To use the EST passphrase in your clients, configure it as the EST password. The EST username can be set to any arbitrary value.
- Use the appropriate client certificates for invoking the EST endpoints.
- For `simpleenroll`, use the bootstrapped/manufacturer client certificate.
- For `simplereenroll`, use a valid EST-issued client certificate.
- When configuring the PKCS#12 objects for the client certificates, only include the leaf certificate and the private key.

View File

@@ -327,10 +327,10 @@ the certificate back to the intermediate CA.
At the moment, Infisical only supports CA renewal via same key pair. We
anticipate supporting CA renewal via new key pair in the coming month.
</Accordion>
<Accordion title="Does Infisical support chaining an Intermediate CA to an external Root CA?">
<Accordion title="Does Infisical support chaining an Intermediate CA to an external CA?">
Yes. You may obtain a CSR from the Intermediate CA and use it to generate a
certificate from your external Root CA. The certificate, along with the Root
CA certificate, can be imported back to the Intermediate CA as part of the
CA installation step.
certificate from your external CA. The certificate, along with the external
CA certificate chain, can be imported back to the Intermediate CA as part of
the CA installation step.
</Accordion>
</AccordionGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Some files were not shown because too many files have changed in this diff Show More