Compare commits

..

133 Commits

Author SHA1 Message Date
Sheen
2be10b5f9d Merge pull request #3503 from Infisical/feat/add-support-for-eddsa-jwt-alg
feat: add support for eddsa jwt alg for oidc
2025-04-29 03:27:58 +08:00
Maidul Islam
3b6e35e13c Merge pull request #3505 from akhilmhdh/feat/cache-jitter
feat: increased secret caching to 10mins with jitter of 2min
2025-04-28 12:16:00 -07:00
=
fcf984965e feat: increased secret caching to 10mins with jitter of 2min 2025-04-29 00:36:39 +05:30
x032205
a69ce50da9 Merge pull request #3495 from Infisical/ENG-2656
feat(login): Update all SSO login methods to use PKCE
2025-04-28 14:33:02 -04:00
Sheen Capadngan
1b798bd5d5 misc: fixed casing 2025-04-29 02:08:13 +08:00
Sheen Capadngan
bd3ebe75c9 feat: add support for eddsa jwt alg for oidc 2025-04-29 02:05:19 +08:00
Maidul Islam
0f2b8e4266 Update github-org-sync.mdx 2025-04-28 14:04:02 -04:00
x
c4ae8f2987 Remove false comment 2025-04-28 13:30:06 -04:00
x
b50a022d11 PKCE check logic fix 2025-04-28 13:28:47 -04:00
x
8a035c8d82 check if OIDC provider supports PKCE before applying it 2025-04-28 12:51:18 -04:00
x
03d7f9f786 scope fix for google strategy 2025-04-28 12:17:04 -04:00
x
1b3e8b0a1c fixed merge conflicts 2025-04-28 10:52:12 -04:00
Sheen
6a26a11cbb Merge pull request #3471 from Infisical/feat/add-support-for-org-sso-bypass-for-sso
feat: enabled sso (google, gitlab, github) to bypass org sso
2025-04-28 22:35:53 +08:00
Maidul Islam
d673c8d8e9 Merge pull request #3498 from akhilmhdh/feat/gh-sync
feat: github org sync
2025-04-28 07:26:07 -07:00
=
b39c7070b5 feat: linted merge issues 2025-04-28 19:51:10 +05:30
=
fa3dd03074 feat: updated review comments by @sheen 2025-04-28 19:48:57 +05:30
=
ee40ffd304 feat: changed get user to get org membership details 2025-04-28 19:48:56 +05:30
=
d3d76467ac feat: addressed rabbit and reptile feedback 2025-04-28 19:48:56 +05:30
=
58940f31e3 docs: added doc for github org sync 2025-04-28 19:48:56 +05:30
=
6d2175cf9f feat: completed github org sync 2025-04-28 19:48:56 +05:30
Maidul Islam
dbb0b28453 Merge pull request #3494 from Infisical/fix/moveablePermissionList
feat(project-permissions): allow users to sort permissions on the UI
2025-04-28 07:14:57 -07:00
Daniel Hougaard
225862aed8 Merge pull request #3453 from Infisical/daniel/reminders
feat(reminders): specify recipients
2025-04-28 18:14:23 +04:00
Maidul Islam
8d1bd6aabb Merge pull request #3447 from akhilmhdh/feat/assume-role
Implemented project permission impersonation
2025-04-28 06:59:09 -07:00
Maidul Islam
740c650441 fix import 2025-04-28 09:54:02 -04:00
BlackMagiq
78ccb5acb7 Merge pull request #3497 from Infisical/ssh-host-alias
Infisical SSH: Add Alias Field to SSH Hosts
2025-04-28 06:41:29 -07:00
Maidul Islam
e9aa8b317b Merge branch 'main' into feat/assume-role 2025-04-28 06:33:26 -07:00
=
7b42f666f9 feat: updated files on review changes 2025-04-28 18:56:17 +05:30
Maidul Islam
8a0cfa34d2 Merge pull request #3501 from Infisical/fix-kms-memory-leak
Fix KMS memory leak
2025-04-28 05:02:26 -07:00
Maidul Islam
ca9825c1fe remove unused logger 2025-04-28 07:59:00 -04:00
Maidul Islam
1dfc9511c1 throw only error and remove bool return 2025-04-28 07:55:33 -04:00
Maidul Islam
694ab35f53 Fix KMS memory leak
Adds a clean up method because KMS clients like GCP use a persistent connection snd if not closed, will continue to eat up the memory.
2025-04-28 07:48:31 -04:00
Tuan Dang
44ae0519d1 Revise ssh host alias field handling/validation 2025-04-27 14:34:26 -07:00
Tuan Dang
3d89a7f45d Revise ssh host alias PR 2025-04-26 18:18:22 -07:00
Tuan Dang
de63c8cb6c Add alias field to ssh hosts for improved ux 2025-04-26 18:04:21 -07:00
Scott Wilson
632572f7c3 Merge pull request #3452 from Infisical/ldaps-connection-and-password-rotation
Feature: LDAP Connection and Password Rotation
2025-04-26 09:13:08 -07:00
Daniel Hougaard
0a5f6274f5 Update CreateReminderForm.tsx 2025-04-26 05:56:11 +04:00
Daniel Hougaard
11ee13676d fix: deletion corner cases 2025-04-26 05:55:25 +04:00
Daniel Hougaard
e7783fe6cc requested changes & edge cases 2025-04-26 05:19:02 +04:00
Scott Wilson
a524690d01 deconflict merge 2025-04-25 17:20:30 -07:00
carlosmonastyrski
2e459c161d feat(project-permissions): type fix 2025-04-25 19:51:08 -03:00
x
680f1a2230 Merge branch 'main' into ENG-2656 2025-04-25 18:46:05 -04:00
x
68e21ba8ce PKCE for Github, Gitlab, Google, and OIDC SSO 2025-04-25 18:45:23 -04:00
carlosmonastyrski
1e9722474f feat(project-permissions): allow users to sort permissions on the UI 2025-04-25 19:35:42 -03:00
Scott Wilson
f93edbb37f Merge pull request #3493 from Infisical/improve-aws-connection-error-propagation
improvement(app-connections): Improve AWS Connection Error Propagation
2025-04-25 15:25:55 -07:00
Scott Wilson
fa8154ecdd improvement: add undefined handling 2025-04-25 15:06:16 -07:00
Scott Wilson
d977092502 improvement: improve validate aws connection error propagation 2025-04-25 15:05:22 -07:00
Andrey
cceb29b93a Merge pull request #3476 from Infisical/ENG-2625
feat(secret-sync): TeamCity App Connection & Secret Sync
2025-04-25 15:44:37 -04:00
carlosmonastyrski
02b44365f1 Merge pull request #3470 from Infisical/feat/awsSecretRotationV2
feat(secret-rotation-v2): Add AWS IAM User Secret rotation
2025-04-25 16:43:22 -03:00
carlosmonastyrski
b506393765 feat(aws-iam-rotation): docs improvements 2025-04-25 16:35:57 -03:00
carlosmonastyrski
204269a10d Merge pull request #3480 from Infisical/feat/paginationAndFilterOnProjectMembers
feat(project-members): Persist pagination setting and add role filtering
2025-04-25 14:51:05 -03:00
BlackMagiq
cf1f83aaa3 Merge pull request #3446 from Infisical/ssh-non-interactive
Improvements to Infisical V2: Support for Non-Interactive Mode, Updating Default SSH CAs.
2025-04-25 10:15:06 -07:00
Andrey
7894181234 Merge pull request #3490 from Infisical/ENG-2546
feat(auth): Persist pre-login-redirect path and redirect after login
2025-04-25 13:12:46 -04:00
Tuan Dang
0c214a2f26 Adjust CLI flags to be dash-case 2025-04-25 10:03:51 -07:00
Tuan Dang
f5862cbb9a Merge 2025-04-25 09:32:48 -07:00
Tuan Dang
bb699ecb5f Merge remote-tracking branch 'origin' into ssh-non-interactive 2025-04-25 09:31:39 -07:00
x
04b20ed11d feat(auth): Persist pre-login-redirect path and redirect after login 2025-04-25 12:09:18 -04:00
Sheen
cd1e2af9bf Merge pull request #3489 from Infisical/feat/add-user-get-token-and-revamp-session-management
feat: add user get token CLI and revamp session management
2025-04-25 23:45:38 +08:00
carlosmonastyrski
7a4a877e39 feat(aws-iam-rotation): remove credentials validation due to excesive await time 2025-04-25 12:38:41 -03:00
carlosmonastyrski
8f670bde88 feat(aws-iam-rotation): add credentials validation 2025-04-25 12:06:30 -03:00
carlosmonastyrski
ff9011c899 feat(aws-iam-rotation): add view credentials component 2025-04-25 11:23:43 -03:00
carlosmonastyrski
57c96abe03 feat(aws-iam-rotation): address PR comments 2025-04-25 11:01:35 -03:00
Sheen Capadngan
178acc412d misc: added optional accesS 2025-04-25 20:52:55 +08:00
Sheen Capadngan
b0288c49c0 feat: add user get token CLI and revamp session management 2025-04-25 20:43:20 +08:00
carlosmonastyrski
f5bb0d4a86 Merge pull request #3484 from Infisical/fix/dynamicSecretSqlErrorPropagation
fix(dynamic-secret): improve error propagation and add FAQ to docs
2025-04-25 08:41:42 -03:00
x
7699705334 tiny encodeURIComponent tweak 2025-04-24 23:36:11 -04:00
x
7c49f6e302 review fixes 2025-04-24 23:30:35 -04:00
Scott Wilson
b329b5aa4b improvements: address feedback 2025-04-24 19:35:56 -07:00
x
0882c181d0 docs(native-integrations): Add deprication warnings on Windmill + TeamCity 2025-04-24 21:55:44 -04:00
x
8672dd641a Merge branch 'main' into ENG-2625 2025-04-24 21:26:05 -04:00
Maidul Islam
c613bb642e Merge pull request #3485 from Infisical/daniel/kms-logs
fix(kms): better error logs
2025-04-24 17:06:01 -07:00
Daniel Hougaard
90fdba0b77 Update kms-service.ts 2025-04-25 04:04:26 +04:00
Daniel Hougaard
795ce11062 Update kms-service.ts 2025-04-25 04:00:14 +04:00
Daniel Hougaard
2d4adfc651 fix(kms): better error logs 2025-04-25 03:54:59 +04:00
carlosmonastyrski
cb826f1a77 fix(dynamic-secret): improve error propagation and add FAQ to docs 2025-04-24 19:21:30 -03:00
Maidul Islam
55f6a06440 Merge pull request #2718 from akhilmhdh/doc/infisical-package
docs: added new docs for infisical package installation instructions
2025-04-24 14:18:07 -07:00
Maidul Islam
a19e5ff905 add min version 2025-04-24 14:16:56 -07:00
Maidul Islam
dccada8a12 Update docs/self-hosting/deployment-options/native/linux-package/installation.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:13:59 -07:00
Maidul Islam
68bbff455f Update docs/self-hosting/overview.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:12:59 -07:00
Maidul Islam
fcb59a1482 Update docs/self-hosting/overview.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:12:45 -07:00
Maidul Islam
b92bc2183a Update docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:12:27 -07:00
Maidul Islam
aff318cf3c Merge branch 'main' into doc/infisical-package 2025-04-24 14:12:01 -07:00
Maidul Islam
c97a3f07a7 update linux docs 2025-04-24 14:10:21 -07:00
Scott Wilson
e0dc2dd6d8 improvements: address feedback 2025-04-24 13:44:43 -07:00
carlosmonastyrski
8bf5b0f457 Merge pull request #3481 from Infisical/fix/AddDeleteProjectProtectedTooltip
fix(delete-project): Add tooltip for delete project button when it has protection enabled
2025-04-24 12:59:35 -03:00
carlosmonastyrski
4973447676 feat(project-members): PR suggestions improvements 2025-04-24 12:21:19 -03:00
carlosmonastyrski
bd2e2b7931 feat(project-members): PR suggestions improvements 2025-04-24 12:14:06 -03:00
Andrey
13b7729af8 Merge pull request #3472 from Infisical/ENG-2618
Admin SSO bypass (break-glass login) sends out email to all org admins + creates audit log
2025-04-24 10:37:00 -04:00
x
e25c1199bc Made email URL use SITE_URL 2025-04-24 10:24:42 -04:00
Akhil Mohan
6b3726957a Merge pull request #3443 from akhilmhdh/doc/sql-change
Updated doc to have europe infisical aws account id
2025-04-24 19:07:43 +05:30
carlosmonastyrski
aa893a40a9 feat(project-members): Persist pagination setting and add role filtering 2025-04-24 10:06:09 -03:00
x
d019011822 Made findOrgMembersByUsername use replicaNode to stay consistent 2025-04-23 19:53:14 -04:00
x
8bd21ffa63 Attached settings URL to email, actor no longer a recipient, removed error handling for email send, used read replica node for findOrgMembersByRole 2025-04-23 19:46:25 -04:00
x
23df78eff8 feat(secret-sync): Only import secrets that have a value from destination to infisical: 2025-04-23 18:57:08 -04:00
x
84255d1b26 remove debug logs, update comments, other nitpicks 2025-04-23 18:44:14 -04:00
x
3a6b2a593b Merge branch 'main' into ENG-2625 2025-04-23 17:59:34 -04:00
x
d3ee30f5e6 feat(secret-sync): TeamCity App Connection & Secret Sync 2025-04-23 17:58:59 -04:00
Daniel Hougaard
9ea6eca560 requested changes 2025-04-23 21:40:01 +04:00
Scott Wilson
33dea34061 chore: removed unused pick 2025-04-22 18:51:40 -07:00
Scott Wilson
da68073e86 chore: revert secret rotation flag 2025-04-22 18:06:44 -07:00
Scott Wilson
7bd312a287 improvements: update regex checks 2025-04-22 17:57:59 -07:00
Scott Wilson
d61e6752d6 Merge branch 'main' into ldaps-connection-and-password-rotation 2025-04-22 17:42:48 -07:00
Scott Wilson
636aee2ea9 improvements: address feedback 2025-04-22 17:36:18 -07:00
carlosmonastyrski
5819b8c576 PR fix suggestions for aws secret rotations 2025-04-22 17:40:15 -03:00
Sheen Capadngan
d5888f9de7 misc: only append isAdminLogin query param when relevant 2025-04-23 03:27:22 +08:00
Sheen Capadngan
1590b528bf misc: used url search params 2025-04-23 03:07:50 +08:00
x
a838f84601 Revert license overwrites, fix type errors, add error handling to email function 2025-04-22 14:58:17 -04:00
x
a32b590dc5 Merge branch 'main' into ENG-2618 2025-04-22 14:37:22 -04:00
x
b330fdbc58 Admin SSO bypass (breakglass login) sends out email to all org admins + creates audit log 2025-04-22 14:36:31 -04:00
Sheen Capadngan
75f1ce7b86 feat: enabled sso to bypass org sso 2025-04-23 02:28:58 +08:00
carlosmonastyrski
b85809293c Lint fix 2025-04-22 13:53:56 -03:00
carlosmonastyrski
f143d8c358 Merge branch 'main' into feat/awsSecretRotationV2 2025-04-22 13:46:35 -03:00
carlosmonastyrski
2e3330bf69 Add AWS secret rotation V2 2025-04-22 13:26:48 -03:00
=
a80520e425 feat: removed all impersonate word in ui 2025-04-21 23:29:25 +05:30
=
4aa3552060 feat: fixed ts issues 2025-04-21 21:30:28 +05:30
=
40781949a6 feat: updated ui based on feedback 2025-04-21 20:02:23 +05:30
=
2ee423174a feat: updated code by rabbit, reptile and maidul changes 2025-04-21 18:43:21 +05:30
=
649f7b560f feat: added audit log for assume 2025-04-21 18:43:21 +05:30
=
7219ba3b46 feat: implemented user role impersonation 2025-04-21 18:43:21 +05:30
Daniel Hougaard
6e65656360 Update CreateReminderForm.tsx 2025-04-19 07:15:29 +04:00
Daniel Hougaard
e0491c2056 Update types.ts 2025-04-19 07:11:22 +04:00
Daniel Hougaard
b8db15563a Update 20250419004044_secret-reminder-recipients.ts 2025-04-19 07:07:45 +04:00
Daniel Hougaard
9982ade219 feat(reminders): specify recipients 2025-04-19 06:59:22 +04:00
Scott Wilson
9032bbe514 feature: ldap connection and password rotation 2025-04-18 17:55:03 -07:00
Tuan Dang
1ea8e5a81e Add frontend uniqueness check for ssh hostnames 2025-04-18 15:25:13 -07:00
Tuan Dang
42aa3c3d46 Remove extra tx in ssh nullable ca defaults migration, update ssh docs 2025-04-18 11:06:59 -07:00
Tuan Dang
184d353de5 Update infisical ssh docs to clarify ssh connect command in different modes 2025-04-17 23:29:20 -07:00
Tuan Dang
b2360f9cc8 Reuse writeToFile fn in ssh connect command 2025-04-17 23:12:44 -07:00
Tuan Dang
846a5a6e19 impl improvements according to greptile 2025-04-17 23:08:33 -07:00
Tuan Dang
c6cd3a8cc0 Add audit logs to project ssh config endpoints 2025-04-17 23:00:46 -07:00
Tuan Dang
796f5510ca Add cli docs for infisical ssh connect command 2025-04-17 22:40:43 -07:00
Tuan Dang
0265665e83 Make infisical ssh v2 work in non-interactive mode, allow reassignment of default ssh cas 2025-04-17 22:35:25 -07:00
=
79e425d807 feat: updated doc to have europe infisical aws account id 2025-04-17 14:25:55 +05:30
=
c1570930a9 docs: added new docs for infisical package installation instructions 2024-11-11 19:23:31 +05:30
384 changed files with 9458 additions and 1004 deletions

View File

@@ -33,6 +33,7 @@
"@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-paginate-graphql": "^5.2.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
@@ -91,10 +92,10 @@
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"otplib": "^12.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.8.0",
"pg": "^8.11.3",
"pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3",
@@ -135,7 +136,6 @@
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
@@ -7245,47 +7245,247 @@
}
},
"node_modules/@octokit/core": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz",
"integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==",
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
"integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.0.0",
"@octokit/request": "^8.0.2",
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"before-after-hook": "^2.2.0",
"@octokit/auth-token": "^5.0.0",
"@octokit/graphql": "^8.2.2",
"@octokit/request": "^9.2.3",
"@octokit/request-error": "^6.1.8",
"@octokit/types": "^14.0.0",
"before-after-hook": "^3.0.2",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/auth-token": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
"integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/endpoint": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/openapi-types": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz",
"integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==",
"license": "MIT",
"peer": true
},
"node_modules/@octokit/core/node_modules/@octokit/request": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
"integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/endpoint": "^10.1.4",
"@octokit/request-error": "^6.1.8",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^2.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/request-error": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
"integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/types": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz",
"integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/openapi-types": "^25.0.0"
}
},
"node_modules/@octokit/core/node_modules/fast-content-type-parse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"peer": true
},
"node_modules/@octokit/core/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
"license": "ISC",
"peer": true
},
"node_modules/@octokit/endpoint": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/endpoint": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz",
"integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==",
"node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/graphql": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz",
"integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==",
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz",
"integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/request": "^8.0.1",
"@octokit/types": "^12.0.0",
"universal-user-agent": "^6.0.0"
"@octokit/request": "^9.2.3",
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/endpoint": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz",
"integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==",
"license": "MIT",
"peer": true
},
"node_modules/@octokit/graphql/node_modules/@octokit/request": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
"integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/endpoint": "^10.1.4",
"@octokit/request-error": "^6.1.8",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^2.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/request-error": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
"integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/types": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz",
"integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/openapi-types": "^25.0.0"
}
},
"node_modules/@octokit/graphql/node_modules/fast-content-type-parse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"peer": true
},
"node_modules/@octokit/graphql/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
"license": "ISC",
"peer": true
},
"node_modules/@octokit/oauth-authorization-url": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz",
@@ -7380,6 +7580,18 @@
"node": ">= 18"
}
},
"node_modules/@octokit/plugin-paginate-graphql": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.4.tgz",
"integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "9.1.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz",
@@ -7461,28 +7673,14 @@
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@octokit/plugin-throttling": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.1.3.tgz",
"integrity": "sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==",
"dependencies": {
"@octokit/types": "^12.2.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "^5.0.0"
}
},
"node_modules/@octokit/request": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz",
"integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
"integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^9.0.1",
"@octokit/request-error": "^5.1.0",
"@octokit/endpoint": "^9.0.6",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
@@ -7491,9 +7689,10 @@
}
},
"node_modules/@octokit/request-error": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz",
"integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
"integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"deprecation": "^2.0.0",
@@ -7543,6 +7742,59 @@
"node": ">= 18"
}
},
"node_modules/@octokit/rest/node_modules/@octokit/core": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/rest/node_modules/@octokit/graphql": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/rest/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@octokit/rest/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/rest/node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"license": "Apache-2.0"
},
"node_modules/@octokit/types": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz",
@@ -9871,17 +10123,6 @@
"@types/express": "*"
}
},
"node_modules/@types/passport-github": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@types/passport-github/-/passport-github-1.1.12.tgz",
"integrity": "sha512-VJpMEIH+cOoXB694QgcxuvWy2wPd1Oq3gqrg2Y9DMVBYs9TmH9L14qnqPDZsNMZKBDH+SvqRsGZj9SgHYeDgcA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-google-oauth20": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz",
@@ -11654,9 +11895,11 @@
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
"license": "Apache-2.0",
"peer": true
},
"node_modules/big-integer": {
"version": "1.6.52",
@@ -18142,9 +18385,10 @@
"integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA=="
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
@@ -18827,17 +19071,6 @@
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-github": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-gitlab2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
@@ -18873,12 +19106,13 @@
}
},
"node_modules/passport-oauth2": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
"integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"license": "MIT",
"dependencies": {
"base64url": "3.x.x",
"oauth": "0.9.x",
"oauth": "0.10.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
@@ -19667,6 +19901,62 @@
"node": ">=18"
}
},
"node_modules/probot/node_modules/@octokit/core": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/probot/node_modules/@octokit/core/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/probot/node_modules/@octokit/graphql": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/probot/node_modules/@octokit/graphql/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/probot/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/probot/node_modules/@octokit/plugin-retry": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
@@ -19683,6 +19973,28 @@
"@octokit/core": ">=5"
}
},
"node_modules/probot/node_modules/@octokit/plugin-throttling": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz",
"integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.2.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "^5.0.0"
}
},
"node_modules/probot/node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"license": "Apache-2.0"
},
"node_modules/probot/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",

View File

@@ -91,7 +91,6 @@
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
@@ -150,6 +149,7 @@
"@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-paginate-graphql": "^5.2.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
@@ -208,10 +208,10 @@
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"otplib": "^12.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.8.0",
"pg": "^8.11.3",
"pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3",

View File

@@ -5,6 +5,7 @@ import { Redis } from "ioredis";
import { TUsers } from "@app/db/schemas";
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
@@ -14,6 +15,7 @@ import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dy
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
@@ -109,12 +111,14 @@ declare module "@fastify/request-context" {
};
};
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
}
}
declare module "fastify" {
interface Session {
callbackPort: string;
isAdminLogin: boolean;
}
interface FastifyRequest {
@@ -138,6 +142,7 @@ declare module "fastify" {
passportUser: {
isUserCompleted: boolean;
providerAuthToken: string;
externalProviderAccessToken?: string;
};
kmipUser: {
projectId: string;
@@ -241,6 +246,8 @@ declare module "fastify" {
kmipOperation: TKmipOperationServiceFactory;
gateway: TGatewayServiceFactory;
secretRotationV2: TSecretRotationV2ServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@@ -83,6 +83,9 @@ import {
TGitAppOrg,
TGitAppOrgInsert,
TGitAppOrgUpdate,
TGithubOrgSyncConfigs,
TGithubOrgSyncConfigsInsert,
TGithubOrgSyncConfigsUpdate,
TGroupProjectMembershipRoles,
TGroupProjectMembershipRolesInsert,
TGroupProjectMembershipRolesUpdate,
@@ -423,6 +426,11 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
import {
TSecretReminderRecipients,
TSecretReminderRecipientsInsert,
TSecretReminderRecipientsUpdate
} from "@app/db/schemas/secret-reminder-recipients";
declare module "knex" {
namespace Knex {
@@ -994,5 +1002,15 @@ declare module "knex/types/tables" {
TSecretRotationV2SecretMappingsInsert,
TSecretRotationV2SecretMappingsUpdate
>;
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
TSecretReminderRecipients,
TSecretReminderRecipientsInsert,
TSecretReminderRecipientsUpdate
>;
[TableName.GithubOrgSyncConfig]: KnexOriginal.CompositeTableType<
TGithubOrgSyncConfigs,
TGithubOrgSyncConfigsInsert,
TGithubOrgSyncConfigsUpdate
>;
}
}

View File

@@ -0,0 +1,34 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSecretReminderRecipientsTable = await knex.schema.hasTable(TableName.SecretReminderRecipients);
if (!hasSecretReminderRecipientsTable) {
await knex.schema.createTable(TableName.SecretReminderRecipients, (table) => {
table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
table.timestamps(true, true, true);
table.uuid("secretId").notNullable();
table.uuid("userId").notNullable();
table.string("projectId").notNullable();
// Based on userId rather than project membership ID so we can easily extend group support in the future if need be.
// This does however mean we need to manually clean up once a user is removed from a project.
table.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
table.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
table.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
table.index("secretId");
table.unique(["secretId", "userId"]);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasSecretReminderRecipientsTable = await knex.schema.hasTable(TableName.SecretReminderRecipients);
if (hasSecretReminderRecipientsTable) {
await knex.schema.dropTableIfExists(TableName.SecretReminderRecipients);
}
}

View File

@@ -0,0 +1,47 @@
import { Knex } from "knex";
import { ProjectType, TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasDefaultUserCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultUserSshCaId");
const hasDefaultHostCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultHostSshCaId");
if (hasDefaultUserCaCol && hasDefaultHostCaCol) {
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
t.dropForeign(["defaultUserSshCaId"]);
t.dropForeign(["defaultHostSshCaId"]);
});
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
// allow nullable (does not wipe existing values)
t.uuid("defaultUserSshCaId").nullable().alter();
t.uuid("defaultHostSshCaId").nullable().alter();
// re-add with SET NULL behavior (previously CASCADE)
t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
});
}
// (dangtony98): backfill by adding null defaults CAs for all existing Infisical SSH projects
// that do not have an associated ProjectSshConfig record introduced in Infisical SSH V2.
const allProjects = await knex(TableName.Project).where("type", ProjectType.SSH).select("id");
const projectsWithConfig = await knex(TableName.ProjectSshConfig).select("projectId");
const projectIdsWithConfig = new Set(projectsWithConfig.map((config) => config.projectId));
const projectsNeedingConfig = allProjects.filter((project) => !projectIdsWithConfig.has(project.id));
if (projectsNeedingConfig.length > 0) {
const configsToInsert = projectsNeedingConfig.map((project) => ({
projectId: project.id,
defaultUserSshCaId: null,
defaultHostSshCaId: null,
createdAt: new Date(),
updatedAt: new Date()
}));
await knex.batchInsert(TableName.ProjectSshConfig, configsToInsert);
}
}
export async function down(): Promise<void> {}

View File

@@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasAliasColumn = await knex.schema.hasColumn(TableName.SshHost, "alias");
if (!hasAliasColumn) {
await knex.schema.alterTable(TableName.SshHost, (t) => {
t.string("alias").nullable();
t.unique(["projectId", "alias"]);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasAliasColumn = await knex.schema.hasColumn(TableName.SshHost, "alias");
if (hasAliasColumn) {
await knex.schema.alterTable(TableName.SshHost, (t) => {
t.dropUnique(["projectId", "alias"]);
t.dropColumn("alias");
});
}
}

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 hasTable = await knex.schema.hasTable(TableName.GithubOrgSyncConfig);
if (!hasTable) {
await knex.schema.createTable(TableName.GithubOrgSyncConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("githubOrgName").notNullable();
t.boolean("isActive").defaultTo(false);
t.binary("encryptedGithubOrgAccessToken");
t.uuid("orgId").notNullable().unique();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.GithubOrgSyncConfig);
await dropOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig);
}

View File

@@ -20,7 +20,7 @@ export const CertificatesSchema = z.object({
notAfter: z.date(),
revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional(),
altNames: z.string().nullable().optional(),
caCertId: z.string().uuid(),
certificateTemplateId: z.string().uuid().nullable().optional(),
keyUsages: z.string().array().nullable().optional(),

View File

@@ -0,0 +1,24 @@
// 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 GithubOrgSyncConfigsSchema = z.object({
id: z.string().uuid(),
githubOrgName: z.string(),
isActive: z.boolean().default(false).nullable().optional(),
encryptedGithubOrgAccessToken: zodBuffer.nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TGithubOrgSyncConfigs = z.infer<typeof GithubOrgSyncConfigsSchema>;
export type TGithubOrgSyncConfigsInsert = Omit<z.input<typeof GithubOrgSyncConfigsSchema>, TImmutableDBKeys>;
export type TGithubOrgSyncConfigsUpdate = Partial<Omit<z.input<typeof GithubOrgSyncConfigsSchema>, TImmutableDBKeys>>;

View File

@@ -25,6 +25,7 @@ export * from "./external-kms";
export * from "./gateways";
export * from "./git-app-install-sessions";
export * from "./git-app-org";
export * from "./github-org-sync-configs";
export * from "./group-project-membership-roles";
export * from "./group-project-memberships";
export * from "./groups";

View File

@@ -13,7 +13,7 @@ export const KmipOrgServerCertificatesSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
commonName: z.string(),
altNames: z.string(),
altNames: z.string().nullable().optional(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),

View File

@@ -146,7 +146,9 @@ export enum TableName {
KmipOrgServerCertificates = "kmip_org_server_certificates",
KmipClientCertificates = "kmip_client_certificates",
SecretRotationV2 = "secret_rotations_v2",
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings"
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
SecretReminderRecipients = "secret_reminder_recipients",
GithubOrgSyncConfig = "github_org_sync_configs"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@@ -30,9 +30,9 @@ export const OidcConfigsSchema = z.object({
updatedAt: z.date(),
orgId: z.string().uuid(),
lastUsed: z.date().nullable().optional(),
manageGroupMemberships: z.boolean().default(false),
encryptedOidcClientId: zodBuffer,
encryptedOidcClientSecret: zodBuffer,
manageGroupMemberships: z.boolean().default(false),
jwtSignatureAlgorithm: z.string().default("RS256")
});

View File

@@ -23,6 +23,7 @@ export const OrganizationsSchema = z.object({
defaultMembershipRole: z.string().default("member"),
enforceMfa: z.boolean().default(false),
selectedMfaMethod: z.string().nullable().optional(),
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
shouldUseNewPrivilegeSystem: z.boolean().default(true),
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),

View File

@@ -0,0 +1,23 @@
// 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 { TImmutableDBKeys } from "./models";
export const SecretReminderRecipientsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
secretId: z.string().uuid(),
userId: z.string().uuid(),
projectId: z.string()
});
export type TSecretReminderRecipients = z.infer<typeof SecretReminderRecipientsSchema>;
export type TSecretReminderRecipientsInsert = Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>;
export type TSecretReminderRecipientsUpdate = Partial<
Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>
>;

View File

@@ -16,7 +16,8 @@ export const SshHostsSchema = z.object({
userCertTtl: z.string(),
hostCertTtl: z.string(),
userSshCaId: z.string().uuid(),
hostSshCaId: z.string().uuid()
hostSshCaId: z.string().uuid(),
alias: z.string().nullable().optional()
});
export type TSshHosts = z.infer<typeof SshHostsSchema>;

View File

@@ -0,0 +1,124 @@
import { requestContext } from "@fastify/request-context";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/assume-privileges",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
projectId: z.string()
}),
body: z.object({
actorType: z.enum([ActorType.USER, ActorType.IDENTITY]),
actorId: z.string()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => {
if (req.auth.authMode === AuthMode.JWT) {
const payload = await server.services.assumePrivileges.assumeProjectPrivileges({
targetActorType: req.body.actorType,
targetActorId: req.body.actorId,
projectId: req.params.projectId,
actorPermissionDetails: req.permission,
tokenVersionId: req.auth.tokenVersionId
});
const appCfg = getConfig();
void res.setCookie("infisical-project-assume-privileges", payload.assumePrivilegesToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 3600 // 1 hour in seconds
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START,
metadata: {
projectId: req.params.projectId,
requesterEmail: req.auth.user.username,
requesterId: req.auth.user.id,
targetActorType: req.body.actorType,
targetActorId: req.body.actorId,
duration: "1hr"
}
}
});
return { message: "Successfully assumed role" };
}
throw new BadRequestError({ message: "Invalid auth mode" });
}
});
server.route({
method: "DELETE",
url: "/:projectId/assume-privileges",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
projectId: z.string()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => {
const assumedPrivilegeDetails = requestContext.get("assumedPrivilegeDetails");
if (req.auth.authMode === AuthMode.JWT && assumedPrivilegeDetails) {
const appCfg = getConfig();
void res.setCookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
expires: new Date(0)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END,
metadata: {
projectId: req.params.projectId,
requesterEmail: req.auth.user.username,
requesterId: req.auth.user.id,
targetActorId: assumedPrivilegeDetails.actorId,
targetActorType: assumedPrivilegeDetails.actorType
}
}
});
return { message: "Successfully exited assumed role" };
}
throw new BadRequestError({ message: "Invalid auth mode" });
}
});
};

View File

@@ -0,0 +1,129 @@
import { z } from "zod";
import { GithubOrgSyncConfigsSchema } from "@app/db/schemas";
import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const SanitizedGithubOrgSyncSchema = GithubOrgSyncConfigsSchema.pick({
isActive: true,
id: true,
createdAt: true,
updatedAt: true,
orgId: true,
githubOrgName: true
});
const githubOrgNameValidator = zodValidateCharacters([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
export const registerGithubOrgSyncRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
githubOrgName: githubOrgNameValidator(z.string().trim(), "GitHub Org Name"),
githubOrgAccessToken: z.string().trim().max(1000).optional(),
isActive: z.boolean().default(false)
}),
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.createGithubOrgSync({
orgPermission: req.permission,
githubOrgName: req.body.githubOrgName,
githubOrgAccessToken: req.body.githubOrgAccessToken,
isActive: req.body.isActive
});
return { githubOrgSyncConfig };
}
});
server.route({
url: "/",
method: "PATCH",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z
.object({
githubOrgName: githubOrgNameValidator(z.string().trim(), "GitHub Org Name"),
githubOrgAccessToken: z.string().trim().max(1000),
isActive: z.boolean()
})
.partial(),
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.updateGithubOrgSync({
orgPermission: req.permission,
githubOrgName: req.body.githubOrgName,
githubOrgAccessToken: req.body.githubOrgAccessToken,
isActive: req.body.isActive
});
return { githubOrgSyncConfig };
}
});
server.route({
url: "/",
method: "DELETE",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.deleteGithubOrgSync({
orgPermission: req.permission
});
return { githubOrgSyncConfig };
}
});
server.route({
url: "/",
method: "GET",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.getGithubOrgSync({
orgPermission: req.permission
});
return { githubOrgSyncConfig };
}
});
};

View File

@@ -2,12 +2,14 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-templat
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGatewayRouter } from "./gateway-router";
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerKmipRouter } from "./kmip-router";
@@ -45,6 +47,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await projectRouter.register(registerProjectRoleRouter);
await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerTrustedIpRouter);
await projectRouter.register(registerAssumePrivilegeRouter);
},
{ prefix: "/workspace" }
);
@@ -70,6 +73,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
);
await server.register(registerGatewayRouter, { prefix: "/gateways" });
await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" });
await server.register(
async (pkiRouter) => {

View File

@@ -1,7 +1,7 @@
import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionV1Schema
@@ -245,13 +245,22 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
data: z.object({
membership: ProjectMembershipsSchema.extend({
membership: z.object({
id: z.string(),
roles: z
.object({
role: z.string()
})
.array()
}),
assumedPrivilegeDetails: z
.object({
actorId: z.string(),
actorType: z.string(),
actorName: z.string(),
actorEmail: z.string().optional()
})
.optional(),
permissions: z.any().array()
})
})
@@ -259,14 +268,20 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { permissions, membership } = await server.services.projectRole.getUserPermission(
const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission(
req.permission.id,
req.params.projectId,
req.permission.authMethod,
req.permission.orgId
);
return { data: { permissions, membership } };
return {
data: {
permissions,
membership,
assumedPrivilegeDetails
}
};
}
});
};

View File

@@ -7,6 +7,7 @@ import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
import { SSH_HOSTS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -96,10 +97,12 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
hostname: z
.string()
.min(1)
.trim()
.refine((v) => isValidHostname(v), {
message: "Hostname must be a valid hostname"
})
.describe(SSH_HOSTS.CREATE.hostname),
alias: slugSchema({ min: 0, max: 64, field: "alias" }).describe(SSH_HOSTS.CREATE.alias).default(""),
userCertTtl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
@@ -138,6 +141,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
metadata: {
sshHostId: host.id,
hostname: host.hostname,
alias: host.alias ?? null,
userCertTtl: host.userCertTtl,
hostCertTtl: host.hostCertTtl,
loginMappings: host.loginMappings,
@@ -166,12 +170,14 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
body: z.object({
hostname: z
.string()
.trim()
.min(1)
.refine((v) => isValidHostname(v), {
message: "Hostname must be a valid hostname"
})
.optional()
.describe(SSH_HOSTS.UPDATE.hostname),
alias: slugSchema({ min: 0, max: 64, field: "alias" }).describe(SSH_HOSTS.UPDATE.alias).optional(),
userCertTtl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
@@ -208,6 +214,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
metadata: {
sshHostId: host.id,
hostname: host.hostname,
alias: host.alias,
userCertTtl: host.userCertTtl,
hostCertTtl: host.hostCertTtl,
loginMappings: host.loginMappings,

View File

@@ -0,0 +1,19 @@
import {
AwsIamUserSecretRotationGeneratedCredentialsSchema,
AwsIamUserSecretRotationSchema,
CreateAwsIamUserSecretRotationSchema,
UpdateAwsIamUserSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerAwsIamUserSecretRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.AwsIamUserSecret,
server,
responseSchema: AwsIamUserSecretRotationSchema,
createSchema: CreateAwsIamUserSecretRotationSchema,
updateSchema: UpdateAwsIamUserSecretRotationSchema,
generatedCredentialsSchema: AwsIamUserSecretRotationGeneratedCredentialsSchema
});

View File

@@ -1,6 +1,8 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -12,5 +14,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
> = {
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
};

View File

@@ -0,0 +1,19 @@
import {
CreateLdapPasswordRotationSchema,
LdapPasswordRotationGeneratedCredentialsSchema,
LdapPasswordRotationSchema,
UpdateLdapPasswordRotationSchema
} from "@app/ee/services/secret-rotation-v2/ldap-password";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.LdapPassword,
server,
responseSchema: LdapPasswordRotationSchema,
createSchema: CreateLdapPasswordRotationSchema,
updateSchema: UpdateLdapPasswordRotationSchema,
generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema
});

View File

@@ -2,6 +2,8 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -13,7 +15,9 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema
Auth0ClientSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,101 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import {
ProjectPermissionIdentityActions,
ProjectPermissionMemberActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TAssumeProjectPrivilegeDTO } from "./assume-privilege-types";
type TAssumePrivilegeServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TAssumePrivilegeServiceFactory = ReturnType<typeof assumePrivilegeServiceFactory>;
export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: TAssumePrivilegeServiceFactoryDep) => {
const assumeProjectPrivileges = async ({
targetActorType,
targetActorId,
projectId,
actorPermissionDetails,
tokenVersionId
}: TAssumeProjectPrivilegeDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actorPermissionDetails.type,
actorId: actorPermissionDetails.id,
projectId,
actorAuthMethod: actorPermissionDetails.authMethod,
actorOrgId: actorPermissionDetails.orgId,
actionProjectType: ActionProjectType.Any
});
if (targetActorType === ActorType.USER) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionMemberActions.AssumePrivileges,
ProjectPermissionSub.Member
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.AssumePrivileges,
ProjectPermissionSub.Identity
);
}
// check entity is part of project
await permissionService.getProjectPermission({
actor: targetActorType,
actorId: targetActorId,
projectId,
actorAuthMethod: actorPermissionDetails.authMethod,
actorOrgId: actorPermissionDetails.orgId,
actionProjectType: ActionProjectType.Any
});
const appCfg = getConfig();
const assumePrivilegesToken = jwt.sign(
{
tokenVersionId,
actorType: targetActorType,
actorId: targetActorId,
projectId,
requesterId: actorPermissionDetails.id
},
appCfg.AUTH_SECRET,
{ expiresIn: "1hr" }
);
return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken };
};
const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => {
const appCfg = getConfig();
const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as {
tokenVersionId: string;
projectId: string;
requesterId: string;
actorType: ActorType;
actorId: string;
};
if (decodedToken.tokenVersionId !== tokenVersionId) {
throw new ForbiddenRequestError({ message: "Invalid token version" });
}
return decodedToken;
};
return {
assumeProjectPrivileges,
verifyAssumePrivilegeToken
};
};

View File

@@ -0,0 +1,10 @@
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
export type TAssumeProjectPrivilegeDTO = {
targetActorType: ActorType.USER | ActorType.IDENTITY;
targetActorId: string;
projectId: string;
tokenVersionId: string;
actorPermissionDetails: OrgServiceActor;
};

View File

@@ -234,6 +234,7 @@ export enum EventType {
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@@ -248,6 +249,8 @@ export enum EventType {
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
INTEGRATION_SYNCED = "integration-synced",
CREATE_CMEK = "create-cmek",
UPDATE_CMEK = "update-cmek",
@@ -317,7 +320,9 @@ export enum EventType {
DELETE_SECRET_ROTATION = "delete-secret-rotation",
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
PROJECT_ACCESS_REQUEST = "project-access-request"
PROJECT_ACCESS_REQUEST = "project-access-request",
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
}
export const filterableSecretEvents: EventType[] = [
@@ -1491,6 +1496,7 @@ interface CreateSshHost {
metadata: {
sshHostId: string;
hostname: string;
alias: string | null;
userCertTtl: string;
hostCertTtl: string;
loginMappings: {
@@ -1509,6 +1515,7 @@ interface UpdateSshHost {
metadata: {
sshHostId: string;
hostname?: string;
alias?: string | null;
userCertTtl?: string;
hostCertTtl?: string;
loginMappings?: {
@@ -1907,6 +1914,11 @@ interface OrgAdminAccessProjectEvent {
}; // no metadata yet
}
interface OrgAdminBypassSSOEvent {
type: EventType.ORG_ADMIN_BYPASS_SSO;
metadata: Record<string, string>; // no metadata yet
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
@@ -1986,6 +1998,25 @@ interface GetProjectSlackConfig {
id: string;
};
}
interface GetProjectSshConfig {
type: EventType.GET_PROJECT_SSH_CONFIG;
metadata: {
id: string;
projectId: string;
};
}
interface UpdateProjectSshConfig {
type: EventType.UPDATE_PROJECT_SSH_CONFIG;
metadata: {
id: string;
projectId: string;
defaultUserSshCaId?: string | null;
defaultHostSshCaId?: string | null;
};
}
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
@@ -2425,6 +2456,29 @@ interface ProjectAccessRequestEvent {
};
}
interface ProjectAssumePrivilegesEvent {
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START;
metadata: {
projectId: string;
requesterId: string;
requesterEmail: string;
targetActorType: ActorType;
targetActorId: string;
duration: string;
};
}
interface ProjectAssumePrivilegesExitEvent {
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END;
metadata: {
projectId: string;
requesterId: string;
requesterEmail: string;
targetActorType: ActorType;
targetActorId: string;
};
}
interface SetupKmipEvent {
type: EventType.SETUP_KMIP;
metadata: {
@@ -2656,6 +2710,7 @@ export type Event =
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent
| OrgAdminBypassSSOEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
@@ -2670,6 +2725,8 @@ export type Event =
| GetSlackIntegration
| UpdateProjectSlackConfig
| GetProjectSlackConfig
| GetProjectSshConfig
| UpdateProjectSshConfig
| IntegrationSyncedEvent
| CreateCmekEvent
| UpdateCmekEvent
@@ -2727,6 +2784,8 @@ export type Event =
| KmipOperationLocateEvent
| KmipOperationRegisterEvent
| ProjectAccessRequestEvent
| ProjectAssumePrivilegesEvent
| ProjectAssumePrivilegesExitEvent
| CreateSecretRequestEvent
| SecretApprovalRequestReview
| GetSecretRotationsEvent

View File

@@ -130,7 +130,17 @@ export const dynamicSecretLeaseServiceFactory = ({
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" });
}
const { entityId, data } = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
let result;
try {
result = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
} catch (error: unknown) {
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
throw new BadRequestError({ message: error.sqlMessage as string });
}
throw error;
}
const { entityId, data } = result;
const dynamicSecretLease = await dynamicSecretLeaseDAL.create({
expireAt,
version: 1,

View File

@@ -83,18 +83,26 @@ export const externalKmsServiceFactory = ({
throw error;
});
// if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput);
try {
// if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput);
await externalKms.validateConnection();
await externalKms.validateConnection();
} finally {
await externalKms.cleanup();
}
}
break;
case KmsProviders.Gcp:
{
const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs });
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(provider.inputs);
try {
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(provider.inputs);
} finally {
await externalKms.cleanup();
}
}
break;
default:
@@ -186,8 +194,12 @@ export const externalKmsServiceFactory = ({
);
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
try {
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
} finally {
await externalKms.cleanup();
}
}
break;
case KmsProviders.Gcp:
@@ -197,8 +209,12 @@ export const externalKmsServiceFactory = ({
);
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput });
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
try {
await externalKms.validateConnection();
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
} finally {
await externalKms.cleanup();
}
}
break;
default:
@@ -368,7 +384,11 @@ export const externalKmsServiceFactory = ({
const fetchGcpKeys = async ({ credential, gcpRegion }: Pick<TExternalKmsGcpSchema, "credential" | "gcpRegion">) => {
const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } });
return externalKms.getKeysList();
try {
return await externalKms.getKeysList();
} finally {
await externalKms.cleanup();
}
};
return {

View File

@@ -102,10 +102,19 @@ export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Pro
return { data: Buffer.from(decryptionCommand.Plaintext) };
};
const cleanup = async () => {
try {
awsClient.destroy();
} catch (error) {
throw new Error("Failed to cleanup AWS KMS client", { cause: error });
}
};
return {
generateInputKmsKey,
validateConnection,
encrypt,
decrypt
decrypt,
cleanup
};
};

View File

@@ -45,6 +45,14 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
}
};
const cleanup = async () => {
try {
await gcpKmsClient.close();
} catch (error) {
throw new Error("Failed to cleanup GCP KMS client", { cause: error });
}
};
// Used when adding the KMS to fetch the list of keys in specified region
const getKeysList = async () => {
try {
@@ -108,6 +116,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
validateConnection,
getKeysList,
encrypt,
decrypt
decrypt,
cleanup
};
};

View File

@@ -98,4 +98,5 @@ export type TExternalKmsProviderFns = {
validateConnection: () => Promise<boolean>;
encrypt: (data: Buffer) => Promise<{ encryptedBlob: Buffer }>;
decrypt: (encryptedBlob: Buffer) => Promise<{ data: Buffer }>;
cleanup: () => Promise<void>;
};

View File

@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TGithubOrgSyncDALFactory = ReturnType<typeof githubOrgSyncDALFactory>;
export const githubOrgSyncDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.GithubOrgSyncConfig);
return orm;
};

View File

@@ -0,0 +1,354 @@
import { ForbiddenError } from "@casl/ability";
import { Octokit } from "@octokit/core";
import { paginateGraphQL } from "@octokit/plugin-paginate-graphql";
import { Octokit as OctokitRest } from "@octokit/rest";
import { OrgMembershipRole } from "@app/db/schemas";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TGroupDALFactory } from "../group/group-dal";
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal";
import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types";
const OctokitWithPlugin = Octokit.plugin(paginateGraphQL);
type TGithubOrgSyncServiceFactoryDep = {
githubOrgSyncDAL: TGithubOrgSyncDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"findGroupMembershipsByUserIdInOrg" | "insertMany" | "delete"
>;
groupDAL: Pick<TGroupDALFactory, "insertMany" | "transaction" | "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TGithubOrgSyncServiceFactory = ReturnType<typeof githubOrgSyncServiceFactory>;
export const githubOrgSyncServiceFactory = ({
githubOrgSyncDAL,
permissionService,
kmsService,
userGroupMembershipDAL,
groupDAL,
licenseService
}: TGithubOrgSyncServiceFactoryDep) => {
const createGithubOrgSync = async ({
githubOrgName,
orgPermission,
githubOrgAccessToken,
isActive
}: TCreateGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId);
if (!plan.githubOrgSync) {
throw new BadRequestError({
message:
"Failed to create github organization team sync due to plan restriction. Upgrade plan to create github organization sync."
});
}
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (existingConfig)
throw new BadRequestError({
message: `Organization ${orgPermission.orgId} already has GitHub Organization sync config.`
});
const octokit = new OctokitRest({
auth: githubOrgAccessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const { data } = await octokit.rest.orgs.get({
org: githubOrgName
});
if (data.login.toLowerCase() !== githubOrgName.toLowerCase())
throw new BadRequestError({ message: "Invalid GitHub organisation" });
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgPermission.orgId
});
const config = await githubOrgSyncDAL.create({
orgId: orgPermission.orgId,
githubOrgName,
isActive,
encryptedGithubOrgAccessToken: githubOrgAccessToken
? encryptor({ plainText: Buffer.from(githubOrgAccessToken) }).cipherTextBlob
: null
});
return config;
};
const updateGithubOrgSync = async ({
githubOrgName,
orgPermission,
githubOrgAccessToken,
isActive
}: TUpdateGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId);
if (!plan.githubOrgSync) {
throw new BadRequestError({
message:
"Failed to update github organization team sync due to plan restriction. Upgrade plan to update github organization sync."
});
}
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (!existingConfig)
throw new BadRequestError({
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
});
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgPermission.orgId
});
const newData = {
githubOrgName: githubOrgName || existingConfig.githubOrgName,
githubOrgAccessToken:
githubOrgAccessToken ||
(existingConfig.encryptedGithubOrgAccessToken
? decryptor({ cipherTextBlob: existingConfig.encryptedGithubOrgAccessToken }).toString()
: null)
};
if (githubOrgName || githubOrgAccessToken) {
const octokit = new OctokitRest({
auth: newData.githubOrgAccessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const { data } = await octokit.rest.orgs.get({
org: newData.githubOrgName
});
if (data.login.toLowerCase() !== newData.githubOrgName.toLowerCase())
throw new BadRequestError({ message: "Invalid GitHub organisation" });
}
const config = await githubOrgSyncDAL.updateById(existingConfig.id, {
orgId: orgPermission.orgId,
githubOrgName: newData.githubOrgName,
isActive,
encryptedGithubOrgAccessToken: newData.githubOrgAccessToken
? encryptor({ plainText: Buffer.from(newData.githubOrgAccessToken) }).cipherTextBlob
: null
});
return config;
};
const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId);
if (!plan.githubOrgSync) {
throw new BadRequestError({
message:
"Failed to delete github organization team sync due to plan restriction. Upgrade plan to delete github organization sync."
});
}
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (!existingConfig)
throw new BadRequestError({
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
});
const config = await githubOrgSyncDAL.deleteById(existingConfig.id);
return config;
};
const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (!existingConfig)
throw new NotFoundError({
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
});
return existingConfig;
};
const syncUserGroups = async (orgId: string, userId: string, accessToken: string) => {
const config = await githubOrgSyncDAL.findOne({ orgId });
if (!config || !config?.isActive) return;
const infisicalUserGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(userId, orgId);
const infisicalUserGroupSet = new Set(infisicalUserGroups.map((el) => el.groupName));
const octoRest = new OctokitRest({
auth: accessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const { data: userOrgMembershipDetails } = await octoRest.rest.orgs
.getMembershipForAuthenticatedUser({
org: config.githubOrgName
})
.catch((err) => {
logger.error(err, "User not part of GitHub synced organization");
throw new BadRequestError({ message: "User not part of GitHub synced organization" });
});
const username = userOrgMembershipDetails?.user?.login;
if (!username) throw new BadRequestError({ message: "User not part of GitHub synced organization" });
const octokit = new OctokitWithPlugin({
auth: accessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const data = await octokit.graphql
.paginate<{
organization: { teams: { totalCount: number; edges: { node: { name: string; description: string } }[] } };
}>(
`
query orgTeams($cursor: String,$org: String!, $username: String!){
organization(login: $org) {
teams(first: 100, userLogins: [$username], after: $cursor) {
totalCount
edges {
node {
name
description
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`,
{
org: config.githubOrgName,
username
}
)
.catch((err) => {
if ((err as Error)?.message?.includes("Although you appear to have the correct authorization credential")) {
throw new BadRequestError({
message:
"Please check your organization have approved Infisical Oauth application. For more info: https://infisical.com/docs/documentation/platform/github-org-sync#troubleshooting"
});
}
throw new BadRequestError({ message: (err as Error)?.message });
});
const {
organization: { teams }
} = data;
const githubUserTeams = teams?.edges?.map((el) => el.node.name.toLowerCase()) || [];
const githubUserTeamSet = new Set(githubUserTeams);
const githubUserTeamOnInfisical = await groupDAL.find({ orgId, $in: { name: githubUserTeams } });
const githubUserTeamOnInfisicalGroupByName = groupBy(githubUserTeamOnInfisical, (i) => i.name);
const newTeams = githubUserTeams.filter(
(el) => !infisicalUserGroupSet.has(el) && !Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el)
);
const updateTeams = githubUserTeams.filter(
(el) => !infisicalUserGroupSet.has(el) && Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el)
);
const removeFromTeams = infisicalUserGroups.filter((el) => !githubUserTeamSet.has(el.groupName));
if (newTeams.length || updateTeams.length || removeFromTeams.length) {
await groupDAL.transaction(async (tx) => {
if (newTeams.length) {
const newGroups = await groupDAL.insertMany(
newTeams.map((newGroupName) => ({
name: newGroupName,
role: OrgMembershipRole.Member,
slug: newGroupName,
orgId
})),
tx
);
await userGroupMembershipDAL.insertMany(
newGroups.map((el) => ({
groupId: el.id,
userId
})),
tx
);
}
if (updateTeams.length) {
await userGroupMembershipDAL.insertMany(
updateTeams.map((el) => ({
groupId: githubUserTeamOnInfisicalGroupByName[el][0].id,
userId
})),
tx
);
}
if (removeFromTeams.length) {
await userGroupMembershipDAL.delete(
{ userId, $in: { groupId: removeFromTeams.map((el) => el.groupId) } },
tx
);
}
});
}
};
return {
createGithubOrgSync,
updateGithubOrgSync,
deleteGithubOrgSync,
getGithubOrgSync,
syncUserGroups
};
};

View File

@@ -0,0 +1,23 @@
import { OrgServiceActor } from "@app/lib/types";
export interface TCreateGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
githubOrgName: string;
githubOrgAccessToken?: string;
isActive?: boolean;
}
export interface TUpdateGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
githubOrgName?: string;
githubOrgAccessToken?: string;
isActive?: boolean;
}
export interface TDeleteGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
}
export interface TGetGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
}

View File

@@ -22,6 +22,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
pitRecovery: false,
ipAllowlisting: false,
rbac: false,
githubOrgSync: false,
customRateLimits: false,
customAlerts: false,
secretAccessInsights: false,

View File

@@ -45,6 +45,7 @@ export type TFeatureSet = {
auditLogsRetentionDays: 0;
auditLogStreams: false;
auditLogStreamLimit: 3;
githubOrgSync: false;
samlSSO: false;
hsm: false;
oidcSSO: false;

View File

@@ -685,10 +685,16 @@ export const oidcConfigServiceFactory = ({
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
});
// Check if the OIDC provider supports PKCE
const codeChallengeMethods = client.issuer.metadata.code_challenge_methods_supported;
const supportsPKCE = Array.isArray(codeChallengeMethods) && codeChallengeMethods.includes("S256");
const strategy = new OpenIdStrategy(
{
client,
passReqToCallback: true
passReqToCallback: true,
usePKCE: supportsPKCE,
params: supportsPKCE ? { code_challenge_method: "S256" } : undefined
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_req: any, tokenSet: TokenSet, cb: any) => {

View File

@@ -8,7 +8,8 @@ export enum OIDCConfigurationType {
export enum OIDCJWTSignatureAlgorithm {
RS256 = "RS256",
HS256 = "HS256",
RS512 = "RS512"
RS512 = "RS512",
EDDSA = "EdDSA"
}
export type TOidcLoginDTO = {

View File

@@ -74,6 +74,7 @@ export enum OrgPermissionSubjects {
IncidentAccount = "incident-contact",
Sso = "sso",
Scim = "scim",
GithubOrgSync = "github-org-sync",
Ldap = "ldap",
Groups = "groups",
Billing = "billing",
@@ -101,6 +102,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.GithubOrgSync]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
@@ -165,6 +167,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.GithubOrgSync).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
@@ -273,6 +279,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);

View File

@@ -551,13 +551,26 @@ export const permissionServiceFactory = ({
};
const getProjectPermission = async <T extends ActorType>({
actor,
actorId,
actor: inputActor,
actorId: inputActorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
let actor = inputActor;
let actorId = inputActorId;
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
if (
assumedPrivilegeDetailsCtx &&
actor === ActorType.USER &&
actorId === assumedPrivilegeDetailsCtx.requesterId &&
projectId === assumedPrivilegeDetailsCtx.projectId
) {
actor = assumedPrivilegeDetailsCtx.actorType;
actorId = assumedPrivilegeDetailsCtx.actorId;
}
switch (actor) {
case ActorType.USER:
return getUserProjectPermission({

View File

@@ -50,7 +50,8 @@ export enum ProjectPermissionIdentityActions {
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
GrantPrivileges = "grant-privileges",
AssumePrivileges = "assume-privileges"
}
export enum ProjectPermissionMemberActions {
@@ -58,7 +59,8 @@ export enum ProjectPermissionMemberActions {
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
GrantPrivileges = "grant-privileges",
AssumePrivileges = "assume-privileges"
}
export enum ProjectPermissionGroupActions {
@@ -714,7 +716,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionMemberActions.Edit,
ProjectPermissionMemberActions.Delete,
ProjectPermissionMemberActions.Read,
ProjectPermissionMemberActions.GrantPrivileges
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionMemberActions.AssumePrivileges
],
ProjectPermissionSub.Member
);
@@ -736,7 +739,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Delete,
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.GrantPrivileges
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionIdentityActions.AssumePrivileges
],
ProjectPermissionSub.Identity
);
@@ -965,7 +969,6 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
@@ -1031,7 +1034,6 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);

View File

@@ -33,6 +33,7 @@ export type TApprovalCreateSecretV2Bridge = {
secretComment?: string;
reminderNote?: string | null;
reminderRepeatDays?: number | null;
secretReminderRecipients?: string[] | null;
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
secretMetadata?: ResourceMetadataDTO;

View File

@@ -0,0 +1,15 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "AWS IAM User Secret",
type: SecretRotation.AwsIamUserSecret,
connection: AppConnection.AWS,
template: {
secretsMapping: {
accessKeyId: "AWS_ACCESS_KEY_ID",
secretAccessKey: "AWS_SECRET_ACCESS_KEY"
}
}
};

View File

@@ -0,0 +1,123 @@
import AWS from "aws-sdk";
import {
TAwsIamUserSecretRotationGeneratedCredentials,
TAwsIamUserSecretRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws";
const getCreateDate = (key: AWS.IAM.AccessKeyMetadata): number => {
return key.CreateDate ? new Date(key.CreateDate).getTime() : 0;
};
export const awsIamUserSecretRotationFactory: TRotationFactory<
TAwsIamUserSecretRotationWithConnection,
TAwsIamUserSecretRotationGeneratedCredentials
> = (secretRotation) => {
const {
parameters: { region, userName },
connection,
secretsMapping
} = secretRotation;
const $rotateClientSecret = async () => {
const { credentials } = await getAwsConnectionConfig(connection, region);
const iam = new AWS.IAM({ credentials });
const { AccessKeyMetadata } = await iam.listAccessKeys({ UserName: userName }).promise();
if (AccessKeyMetadata && AccessKeyMetadata.length > 0) {
// Sort keys by creation date (oldest first)
const sortedKeys = [...AccessKeyMetadata].sort((a, b) => getCreateDate(a) - getCreateDate(b));
// If we already have 2 keys, delete the oldest one
if (sortedKeys.length >= 2) {
const accessId = sortedKeys[0].AccessKeyId || sortedKeys[1].AccessKeyId;
if (accessId) {
await iam
.deleteAccessKey({
UserName: userName,
AccessKeyId: accessId
})
.promise();
}
}
}
const { AccessKey } = await iam.createAccessKey({ UserName: userName }).promise();
return {
accessKeyId: AccessKey.AccessKeyId,
secretAccessKey: AccessKey.SecretAccessKey
};
};
const issueCredentials: TRotationFactoryIssueCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
generatedCredentials,
callback
) => {
const { credentials } = await getAwsConnectionConfig(connection, region);
const iam = new AWS.IAM({ credentials });
await Promise.all(
generatedCredentials.map((generatedCredential) =>
iam
.deleteAccessKey({
UserName: userName,
AccessKeyId: generatedCredential.accessKeyId
})
.promise()
)
);
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAwsIamUserSecretRotationGeneratedCredentials> = (
generatedCredentials
) => {
const secrets = [
{
key: secretsMapping.accessKeyId,
value: generatedCredentials.accessKeyId
},
{
key: secretsMapping.secretAccessKey,
value: generatedCredentials.secretAccessKey
}
];
return secrets;
};
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -0,0 +1,68 @@
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { SecretRotations } from "@app/lib/api-docs";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
export const AwsIamUserSecretRotationGeneratedCredentialsSchema = z
.object({
accessKeyId: z.string(),
secretAccessKey: z.string()
})
.array()
.min(1)
.max(2);
const AwsIamUserSecretRotationParametersSchema = z.object({
userName: z
.string()
.trim()
.min(1, "Client Name Required")
.describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.userName),
region: z.nativeEnum(AWSRegion).describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.region).optional()
});
const AwsIamUserSecretRotationSecretsMappingSchema = z.object({
accessKeyId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.accessKeyId),
secretAccessKey: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.secretAccessKey)
});
export const AwsIamUserSecretRotationTemplateSchema = z.object({
secretsMapping: z.object({
accessKeyId: z.string(),
secretAccessKey: z.string()
})
});
export const AwsIamUserSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AwsIamUserSecret).extend({
type: z.literal(SecretRotation.AwsIamUserSecret),
parameters: AwsIamUserSecretRotationParametersSchema,
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
});
export const CreateAwsIamUserSecretRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.AwsIamUserSecret
).extend({
parameters: AwsIamUserSecretRotationParametersSchema,
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
});
export const UpdateAwsIamUserSecretRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.AwsIamUserSecret
).extend({
parameters: AwsIamUserSecretRotationParametersSchema.optional(),
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema.optional()
});
export const AwsIamUserSecretRotationListItemSchema = z.object({
name: z.literal("AWS IAM User Secret"),
connection: z.literal(AppConnection.AWS),
type: z.literal(SecretRotation.AwsIamUserSecret),
template: AwsIamUserSecretRotationTemplateSchema
});

View File

@@ -0,0 +1,24 @@
import { z } from "zod";
import { TAwsConnection } from "@app/services/app-connection/aws";
import {
AwsIamUserSecretRotationGeneratedCredentialsSchema,
AwsIamUserSecretRotationListItemSchema,
AwsIamUserSecretRotationSchema,
CreateAwsIamUserSecretRotationSchema
} from "./aws-iam-user-secret-rotation-schemas";
export type TAwsIamUserSecretRotation = z.infer<typeof AwsIamUserSecretRotationSchema>;
export type TAwsIamUserSecretRotationInput = z.infer<typeof CreateAwsIamUserSecretRotationSchema>;
export type TAwsIamUserSecretRotationListItem = z.infer<typeof AwsIamUserSecretRotationListItemSchema>;
export type TAwsIamUserSecretRotationWithConnection = TAwsIamUserSecretRotation & {
connection: TAwsConnection;
};
export type TAwsIamUserSecretRotationGeneratedCredentials = z.infer<
typeof AwsIamUserSecretRotationGeneratedCredentialsSchema
>;

View File

@@ -0,0 +1,3 @@
export * from "./aws-iam-user-secret-rotation-constants";
export * from "./aws-iam-user-secret-rotation-schemas";
export * from "./aws-iam-user-secret-rotation-types";

View File

@@ -0,0 +1,3 @@
export * from "./ldap-password-rotation-constants";
export * from "./ldap-password-rotation-schemas";
export * from "./ldap-password-rotation-types";

View File

@@ -0,0 +1,15 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "LDAP Password",
type: SecretRotation.LdapPassword,
connection: AppConnection.LDAP,
template: {
secretsMapping: {
dn: "LDAP_DN",
password: "LDAP_PASSWORD"
}
}
};

View File

@@ -0,0 +1,181 @@
import ldap from "ldapjs";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { logger } from "@app/lib/logger";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { generatePassword } from "../shared/utils";
import {
TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationWithConnection
} from "./ldap-password-rotation-types";
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
export const ldapPasswordRotationFactory: TRotationFactory<
TLdapPasswordRotationWithConnection,
TLdapPasswordRotationGeneratedCredentials
> = (secretRotation, appConnectionDAL, kmsService) => {
const {
connection,
parameters: { dn, passwordRequirements },
secretsMapping
} = secretRotation;
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
try {
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials });
client.unbind();
client.destroy();
} catch (error) {
throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
}
};
const $rotatePassword = async () => {
const { credentials, orgId } = connection;
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
const client = await getLdapConnectionClient(credentials);
const isPersonalRotation = credentials.dn === dn;
const password = generatePassword(passwordRequirements);
let changes: ldap.Change[] | ldap.Change;
switch (credentials.provider) {
case LdapProvider.ActiveDirectory:
{
const encodedPassword = getEncodedPassword(password);
// service account vs personal password rotation require different changes
if (isPersonalRotation) {
const currentEncodedPassword = getEncodedPassword(credentials.password);
changes = [
new ldap.Change({
operation: "delete",
modification: {
type: "unicodePwd",
values: [currentEncodedPassword]
}
}),
new ldap.Change({
operation: "add",
modification: {
type: "unicodePwd",
values: [encodedPassword]
}
})
];
} else {
changes = new ldap.Change({
operation: "replace",
modification: {
type: "unicodePwd",
values: [encodedPassword]
}
});
}
}
break;
default:
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
}
try {
await new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => {
if (err) {
logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`));
} else {
resolve(true);
}
});
});
} finally {
client.unbind();
client.destroy();
}
await $verifyCredentials({ dn, password });
if (isPersonalRotation) {
const updatedCredentials: TLdapConnection["credentials"] = {
...credentials,
password
};
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
});
await appConnectionDAL.updateById(connection.id, { encryptedCredentials });
}
return { dn, password };
};
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotatePassword();
return callback(credentials);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_,
callback
) => {
// we just rotate to a new password, essentially revoking old credentials
await $rotatePassword();
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = await $rotatePassword();
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TLdapPasswordRotationGeneratedCredentials> = (
generatedCredentials
) => {
const secrets = [
{
key: secretsMapping.dn,
value: generatedCredentials.dn
},
{
key: secretsMapping.password,
value: generatedCredentials.password
}
];
return secrets;
};
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -0,0 +1,68 @@
import RE2 from "re2";
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
import { SecretRotations } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const LdapPasswordRotationGeneratedCredentialsSchema = z
.object({
dn: z.string(),
password: z.string()
})
.array()
.min(1)
.max(2);
const LdapPasswordRotationParametersSchema = z.object({
dn: z
.string()
.trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
.min(1, "Distinguished Name (DN) Required")
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
passwordRequirements: PasswordRequirementsSchema.optional()
});
const LdapPasswordRotationSecretsMappingSchema = z.object({
dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn),
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password)
});
export const LdapPasswordRotationTemplateSchema = z.object({
secretsMapping: z.object({
dn: z.string(),
password: z.string()
})
});
export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({
type: z.literal(SecretRotation.LdapPassword),
parameters: LdapPasswordRotationParametersSchema,
secretsMapping: LdapPasswordRotationSecretsMappingSchema
});
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema,
secretsMapping: LdapPasswordRotationSecretsMappingSchema
});
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema.optional(),
secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional()
});
export const LdapPasswordRotationListItemSchema = z.object({
name: z.literal("LDAP Password"),
connection: z.literal(AppConnection.LDAP),
type: z.literal(SecretRotation.LdapPassword),
template: LdapPasswordRotationTemplateSchema
});

View File

@@ -0,0 +1,22 @@
import { z } from "zod";
import { TLdapConnection } from "@app/services/app-connection/ldap";
import {
CreateLdapPasswordRotationSchema,
LdapPasswordRotationGeneratedCredentialsSchema,
LdapPasswordRotationListItemSchema,
LdapPasswordRotationSchema
} from "./ldap-password-rotation-schemas";
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
export type TLdapPasswordRotationListItem = z.infer<typeof LdapPasswordRotationListItemSchema>;
export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & {
connection: TLdapConnection;
};
export type TLdapPasswordRotationGeneratedCredentials = z.infer<typeof LdapPasswordRotationGeneratedCredentialsSchema>;

View File

@@ -1,7 +1,9 @@
export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret"
Auth0ClientSecret = "auth0-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret"
}
export enum SecretRotationStatus {

View File

@@ -4,6 +4,8 @@ import { getConfig } from "@app/lib/config/env";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -18,7 +20,9 @@ import {
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {

View File

@@ -3,12 +3,16 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
};

View File

@@ -14,6 +14,7 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
calculateNextRotationAt,
@@ -77,6 +78,7 @@ import {
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = {
@@ -114,7 +116,9 @@ type TRotationFactoryImplementation = TRotationFactory<
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({
@@ -449,6 +453,18 @@ export const secretRotationV2ServiceFactory = ({
kmsService
);
// even though we have a db constraint we want to check before any rotation of credentials is attempted
// to prevent creation failure after external credentials have been modified
const conflictingRotation = await secretRotationV2DAL.findOne({
name: payload.name,
folderId: folder.id
});
if (conflictingRotation)
throw new BadRequestError({
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"`
});
try {
const currentTime = new Date();

View File

@@ -12,6 +12,20 @@ import {
TAuth0ClientSecretRotationListItem,
TAuth0ClientSecretRotationWithConnection
} from "./auth0-client-secret";
import {
TAwsIamUserSecretRotation,
TAwsIamUserSecretRotationGeneratedCredentials,
TAwsIamUserSecretRotationInput,
TAwsIamUserSecretRotationListItem,
TAwsIamUserSecretRotationWithConnection
} from "./aws-iam-user-secret";
import {
TLdapPasswordRotation,
TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput,
TLdapPasswordRotationListItem,
TLdapPasswordRotationWithConnection
} from "./ldap-password";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput,
@@ -27,26 +41,39 @@ import {
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation } from "./secret-rotation-v2-enums";
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
export type TSecretRotationV2 =
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection;
| TAuth0ClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials;
| TAuth0ClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput;
| TAuth0ClientSecretRotationInput
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem;
| TAuth0ClientSecretRotationListItem
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;

View File

@@ -1,11 +1,16 @@
import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema
Auth0ClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]);

View File

@@ -0,0 +1 @@
export * from "./password-requirements-schema";

View File

@@ -0,0 +1,44 @@
import RE2 from "re2";
import { z } from "zod";
import { SecretRotations } from "@app/lib/api-docs";
export const PasswordRequirementsSchema = z
.object({
length: z
.number()
.min(1, "Password length must be a positive number")
.max(250, "Password length must be less than 250")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length),
required: z.object({
digits: z
.number()
.min(0, "Digit count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits),
lowercase: z
.number()
.min(0, "Lowercase count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase),
uppercase: z
.number()
.min(0, "Uppercase count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase),
symbols: z
.number()
.min(0, "Symbol count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols)
}),
allowedSymbols: z
.string()
.regex(new RE2("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?~]"), "Invalid symbols")
.optional()
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols)
})
.refine((data) => {
return Object.values(data.required).some((count) => count > 0);
}, "At least one character type must be required")
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base);

View File

@@ -1,6 +1,17 @@
import { randomInt } from "crypto";
const DEFAULT_PASSWORD_REQUIREMENTS = {
type TPasswordRequirements = {
length: number;
required: {
lowercase: number;
uppercase: number;
digits: number;
symbols: number;
};
allowedSymbols?: string;
};
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
length: 48,
required: {
lowercase: 1,
@@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = {
allowedSymbols: "-_.~!*"
};
export const generatePassword = () => {
export const generatePassword = (passwordRequirements?: TPasswordRequirements) => {
try {
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",

View File

@@ -33,6 +33,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost),
db.ref("hostname").withSchema(TableName.SshHost),
db.ref("alias").withSchema(TableName.SshHost),
db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
@@ -45,7 +46,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
const grouped = groupBy(rows, (r) => r.sshHostId);
return Object.values(grouped).map((hostRows) => {
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0];
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } =
hostRows[0];
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
@@ -59,6 +61,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
return {
id: sshHostId,
hostname,
alias,
projectId,
userCertTtl,
hostCertTtl,
@@ -87,6 +90,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost),
db.ref("hostname").withSchema(TableName.SshHost),
db.ref("alias").withSchema(TableName.SshHost),
db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
@@ -99,7 +103,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
return Object.values(hostsGrouped).map((hostRows) => {
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
const loginMappingGrouped = groupBy(
hostRows.filter((r) => r.loginUser),
@@ -116,6 +120,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
return {
id: sshHostId,
hostname,
alias,
projectId,
userCertTtl,
hostCertTtl,
@@ -144,6 +149,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost),
db.ref("hostname").withSchema(TableName.SshHost),
db.ref("alias").withSchema(TableName.SshHost),
db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
@@ -155,7 +161,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
if (rows.length === 0) return null;
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
const { sshHostId: id, projectId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
const loginMappingGrouped = groupBy(
rows.filter((r) => r.loginUser),
@@ -173,6 +179,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
id,
projectId,
hostname,
alias,
userCertTtl,
hostCertTtl,
loginMappings,

View File

@@ -6,6 +6,7 @@ export const sanitizedSshHost = SshHostsSchema.pick({
id: true,
projectId: true,
hostname: true,
alias: true,
userCertTtl: true,
hostCertTtl: true,
userSshCaId: true,

View File

@@ -119,6 +119,7 @@ export const sshHostServiceFactory = ({
const createSshHost = async ({
projectId,
hostname,
alias,
userCertTtl,
hostCertTtl,
loginMappings,
@@ -192,6 +193,7 @@ export const sshHostServiceFactory = ({
{
projectId,
hostname,
alias: alias === "" ? null : alias,
userCertTtl,
hostCertTtl,
userSshCaId,
@@ -265,6 +267,7 @@ export const sshHostServiceFactory = ({
const updateSshHost = async ({
sshHostId,
hostname,
alias,
userCertTtl,
hostCertTtl,
loginMappings,
@@ -297,6 +300,7 @@ export const sshHostServiceFactory = ({
sshHostId,
{
hostname,
alias: alias === "" ? null : alias,
userCertTtl,
hostCertTtl
},

View File

@@ -4,6 +4,7 @@ export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
export type TCreateSshHostDTO = {
hostname: string;
alias?: string;
userCertTtl: string;
hostCertTtl: string;
loginMappings: {
@@ -19,6 +20,7 @@ export type TCreateSshHostDTO = {
export type TUpdateSshHostDTO = {
sshHostId: string;
hostname?: string;
alias?: string;
userCertTtl?: string;
hostCertTtl?: string;
loginMappings?: {

View File

@@ -807,6 +807,8 @@ export const RAW_SECRETS = {
tagIds: "The ID of the tags to be attached to the updated secret.",
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.",
secretReminderNote: "Note to be attached in notification email.",
secretReminderRecipients:
"An array of user IDs that will receive the reminder email. If not specified, all project members will receive the reminder email.",
newSecretName: "The new name for the secret."
},
DELETE: {
@@ -1387,6 +1389,7 @@ export const SSH_HOSTS = {
CREATE: {
projectId: "The ID of the project to create the SSH host in.",
hostname: "The hostname of the SSH host.",
alias: "The alias for the SSH host.",
userCertTtl: "The time to live for user certificates issued under this host.",
hostCertTtl: "The time to live for host certificates issued under this host.",
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
@@ -1401,6 +1404,7 @@ export const SSH_HOSTS = {
UPDATE: {
sshHostId: "The ID of the SSH host to update.",
hostname: "The hostname of the SSH host to update to.",
alias: "The alias for the SSH host to update to.",
userCertTtl: "The time to live for user certificates issued under this host to update to.",
hostCertTtl: "The time to live for host certificates issued under this host to update to.",
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
@@ -1857,6 +1861,20 @@ export const AppConnections = {
WINDMILL: {
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
accessToken: "The access token to use to connect with Windmill."
},
LDAP: {
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
password: "The password to bind with for authentication.",
sslRejectUnauthorized:
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
sslCertificate:
"The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate."
},
TEAMCITY: {
instanceUrl: "The TeamCity instance URL to connect with.",
accessToken: "The access token to use to connect with TeamCity."
}
}
};
@@ -1996,6 +2014,10 @@ export const SecretSyncs = {
WINDMILL: {
workspace: "The Windmill workspace to sync secrets to.",
path: "The Windmill workspace path to sync secrets to."
},
TEAMCITY: {
project: "The TeamCity project to sync secrets to.",
buildConfig: "The TeamCity build configuration to sync secrets to."
}
}
};
@@ -2060,6 +2082,26 @@ export const SecretRotations = {
},
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
},
LDAP_PASSWORD: {
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
},
GENERAL: {
PASSWORD_REQUIREMENTS: {
base: "The password requirements to use when generating the new password.",
length: "The length of the password to generate.",
required: {
digits: "The amount of digits to require in the generated password.",
lowercase: "The amount of lowercase characters to require in the generated password.",
uppercase: "The amount of uppercase characters to require in the generated password.",
symbols: "The amount of symbols to require in the generated password."
},
allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").'
}
},
AWS_IAM_USER_SECRET: {
userName: "The name of the client to rotate credentials for.",
region: "The AWS region the client is present in."
}
},
SECRETS_MAPPING: {
@@ -2070,6 +2112,14 @@ export const SecretRotations = {
AUTH0_CLIENT_SECRET: {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
},
LDAP_PASSWORD: {
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
password: "The name of the secret that the rotated password will be mapped to."
},
AWS_IAM_USER_SECRET: {
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
}
}
};

View File

@@ -0,0 +1 @@
export const INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN = "x-infisical-github-auth-access-token";

View File

@@ -2,7 +2,7 @@ export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const secondsToMillis = (seconds: number) => seconds * 1000;
export const applyJitter = (delayMs: number, jitterMs: number) => {
const jitter = Math.floor(Math.random() * (2 * jitterMs)) - jitterMs;
return delayMs + jitter;
export const applyJitter = (delay: number, jitter: number) => {
const jitterTime = Math.floor(Math.random() * (2 * jitter)) - jitter;
return delay + jitterTime;
};

View File

@@ -0,0 +1,3 @@
export const DistinguishedNameRegex =
// DN format, ie; CN=user,OU=users,DC=example,DC=com
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/;

View File

@@ -16,3 +16,17 @@ export const fetchGithubEmails = async (accessToken: string) => {
});
return data;
};
type TGithubUser = {
name?: string;
login: string;
};
export const fetchGithubUser = async (accessToken: string) => {
const { data } = await request.get<TGithubUser>(`${INTEGRATION_GITHUB_API_URL}/user`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return data;
};

View File

@@ -0,0 +1,24 @@
import { requestContext } from "@fastify/request-context";
import fp from "fastify-plugin";
import { AuthMode } from "@app/services/auth/auth-type";
export const injectAssumePrivilege = fp(async (server: FastifyZodProvider) => {
server.addHook("onRequest", async (req, res) => {
const assumeRoleCookie = req.cookies["infisical-project-assume-privileges"];
try {
if (req?.auth?.authMode === AuthMode.JWT && assumeRoleCookie) {
const decodedToken = server.services.assumePrivileges.verifyAssumePrivilegeToken(
assumeRoleCookie,
req.auth.tokenVersionId
);
if (decodedToken) {
requestContext.set("assumedPrivilegeDetails", decodedToken);
}
}
} catch (error) {
req.log.error({ error }, "Failed to verify assume privilege token");
void res.clearCookie("infisical-project-assume-privileges");
}
});
});

View File

@@ -12,6 +12,7 @@ import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-appr
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
import { assumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service";
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
@@ -32,6 +33,8 @@ import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -214,6 +217,7 @@ import { secretFolderServiceFactory } from "@app/services/secret-folder/secret-f
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
import { secretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { secretReminderRecipientsDALFactory } from "@app/services/secret-reminder-recipients/secret-reminder-recipients-dal";
import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { secretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
@@ -248,6 +252,7 @@ import { workflowIntegrationDALFactory } from "@app/services/workflow-integratio
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege";
import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
@@ -417,6 +422,8 @@ export const registerRoutes = async (
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
@@ -427,6 +434,11 @@ export const registerRoutes = async (
serviceTokenDAL,
projectDAL
});
const assumePrivilegeService = assumePrivilegeServiceFactory({
projectDAL,
permissionService
});
const licenseService = licenseServiceFactory({
permissionService,
orgDAL,
@@ -549,6 +561,15 @@ export const registerRoutes = async (
externalGroupOrgRoleMappingDAL
});
const githubOrgSyncConfigService = githubOrgSyncServiceFactory({
licenseService,
githubOrgSyncDAL,
kmsService,
permissionService,
groupDAL,
userGroupMembershipDAL
});
const ldapService = ldapConfigServiceFactory({
ldapConfigDAL,
ldapGroupMapDAL,
@@ -596,7 +617,14 @@ export const registerRoutes = async (
kmsService
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
const loginService = authLoginServiceFactory({
userDAL,
smtpService,
tokenService,
orgDAL,
totpService,
auditLogService
});
const passwordService = authPaswordServiceFactory({
tokenService,
smtpService,
@@ -721,6 +749,7 @@ export const registerRoutes = async (
projectKeyDAL,
projectRoleDAL,
groupProjectDAL,
secretReminderRecipientsDAL,
licenseService
});
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
@@ -954,6 +983,7 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
projectKeyDAL,
projectUserMembershipRoleDAL,
secretReminderRecipientsDAL,
orgService,
resourceMetadataDAL,
secretSyncQueue
@@ -1015,7 +1045,9 @@ export const registerRoutes = async (
projectRoleDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
projectDAL
projectDAL,
identityDAL,
userDAL
});
const snapshotService = secretSnapshotServiceFactory({
@@ -1668,7 +1700,9 @@ export const registerRoutes = async (
kmip: kmipService,
kmipOperation: kmipOperationService,
gateway: gatewayService,
secretRotationV2: secretRotationV2Service
secretRotationV2: secretRotationV2Service,
assumePrivileges: assumePrivilegeService,
githubOrgSync: githubOrgSyncConfigService
});
const cronJobs: CronJob[] = [];
@@ -1689,6 +1723,7 @@ export const registerRoutes = async (
});
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
await server.register(injectAssumePrivilege);
await server.register(injectPermission);
await server.register(injectRateLimits);
await server.register(injectAuditLogInfo);
@@ -1728,30 +1763,6 @@ export const registerRoutes = async (
logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
// 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",

View File

@@ -28,11 +28,16 @@ import {
HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema
} from "@app/services/app-connection/humanitec";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import {
SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema
} from "@app/services/app-connection/teamcity";
import {
SanitizedTerraformCloudConnectionSchema,
TerraformCloudConnectionListItemSchema
@@ -59,7 +64,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options
...SanitizedAuth0ConnectionSchema.options,
...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -76,7 +83,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema
Auth0ConnectionListItemSchema,
LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -59,4 +59,40 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
return { kmsKeys };
}
});
server.route({
method: "GET",
url: `/:connectionId/users`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
iamUsers: z
.object({
UserName: z.string(),
Arn: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const iamUsers = await server.services.appConnection.aws.listIamUsers(
{
connectionId
},
req.permission
);
return { iamUsers };
}
});
};

View File

@@ -1,6 +1,6 @@
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
@@ -9,8 +9,10 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router";
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
@@ -32,5 +34,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.Windmill]: registerWindmillConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
};

View File

@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLdapConnectionSchema,
SanitizedLdapConnectionSchema,
UpdateLdapConnectionSchema
} from "@app/services/app-connection/ldap";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerLdapConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.LDAP,
server,
sanitizedResponseSchema: SanitizedLdapConnectionSchema,
createSchema: CreateLdapConnectionSchema,
updateSchema: UpdateLdapConnectionSchema
});
};

View File

@@ -0,0 +1,60 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateTeamCityConnectionSchema,
SanitizedTeamCityConnectionSchema,
UpdateTeamCityConnectionSchema
} from "@app/services/app-connection/teamcity";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerTeamCityConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.TeamCity,
server,
sanitizedResponseSchema: SanitizedTeamCityConnectionSchema,
createSchema: CreateTeamCityConnectionSchema,
updateSchema: UpdateTeamCityConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string(),
buildTypes: z.object({
buildType: z
.object({
id: z.string(),
name: z.string()
})
.array()
})
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.teamcity.listProjects(connectionId, req.permission);
return projects;
}
});
};

View File

@@ -33,6 +33,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return { message: "Successfully logged out" };
}
});

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -594,6 +594,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(),
secrets: secretRawSchema
.extend({
secretReminderRecipients: z
.object({
user: UsersSchema.pick({ id: true, email: true, username: true }),
id: z.string()
})
.array(),
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@@ -6,6 +7,7 @@ import {
ProjectMembershipsSchema,
ProjectRolesSchema,
ProjectSlackConfigsSchema,
ProjectSshConfigsSchema,
ProjectType,
SecretFoldersSchema,
SortDirection,
@@ -78,7 +80,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
includeGroupMembers: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.transform((value) => value === "true"),
roles: z
.string()
.trim()
.transform(decodeURIComponent)
.refine((value) => {
if (!value) return true;
const slugs = value.split(",");
return slugs.every((slug) => slugify(slug.trim(), { lowercase: true }) === slug.trim());
})
.optional()
}),
params: z.object({
workspaceId: z.string().trim()
@@ -117,13 +129,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const roles = (req.query.roles?.split(",") || []).filter(Boolean);
const users = await server.services.projectMembership.getProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
includeGroupMembers: req.query.includeGroupMembers,
projectId: req.params.workspaceId,
actorOrgId: req.permission.orgId
actorOrgId: req.permission.orgId,
roles
});
return { users };
@@ -623,6 +637,107 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/:workspaceId/ssh-config",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: ProjectSshConfigsSchema.pick({
id: true,
createdAt: true,
updatedAt: true,
projectId: true,
defaultUserSshCaId: true,
defaultHostSshCaId: true
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const sshConfig = await server.services.project.getProjectSshConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: sshConfig.projectId,
event: {
type: EventType.GET_PROJECT_SSH_CONFIG,
metadata: {
id: sshConfig.id,
projectId: sshConfig.projectId
}
}
});
return sshConfig;
}
});
server.route({
method: "PATCH",
url: "/:workspaceId/ssh-config",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
defaultUserSshCaId: z.string().optional(),
defaultHostSshCaId: z.string().optional()
}),
response: {
200: ProjectSshConfigsSchema.pick({
id: true,
createdAt: true,
updatedAt: true,
projectId: true,
defaultUserSshCaId: true,
defaultHostSshCaId: true
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const sshConfig = await server.services.project.updateProjectSshConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: sshConfig.projectId,
event: {
type: EventType.UPDATE_PROJECT_SSH_CONFIG,
metadata: {
id: sshConfig.id,
projectId: sshConfig.projectId,
defaultUserSshCaId: sshConfig.defaultUserSshCaId,
defaultHostSshCaId: sshConfig.defaultHostSshCaId
}
}
});
return sshConfig;
}
});
server.route({
method: "GET",
url: "/:workspaceId/slack-config",

View File

@@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router";
import { registerWindmillSyncRouter } from "./windmill-sync-router";
@@ -27,5 +28,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
[SecretSync.Camunda]: registerCamundaSyncRouter,
[SecretSync.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter
[SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter
};

View File

@@ -23,6 +23,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
@@ -39,7 +40,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
TerraformCloudSyncSchema,
CamundaSyncSchema,
VercelSyncSchema,
WindmillSyncSchema
WindmillSyncSchema,
TeamCitySyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -54,7 +56,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
TerraformCloudSyncListItemSchema,
CamundaSyncListItemSchema,
VercelSyncListItemSchema,
WindmillSyncListItemSchema
WindmillSyncListItemSchema,
TeamCitySyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,17 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
CreateTeamCitySyncSchema,
TeamCitySyncSchema,
UpdateTeamCitySyncSchema
} from "@app/services/secret-sync/teamcity";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerTeamCitySyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.TeamCity,
server,
responseSchema: TeamCitySyncSchema,
createSchema: CreateTeamCitySyncSchema,
updateSchema: UpdateTeamCitySyncSchema
});

View File

@@ -9,15 +9,17 @@
import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session";
import RedisStore from "connect-redis";
import { Strategy as GitHubStrategy } from "passport-github";
import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as OAuth2Strategy } from "passport-oauth2";
import { z } from "zod";
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { fetchGithubEmails } from "@app/lib/requests/github";
import { ms } from "@app/lib/ms";
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types";
@@ -42,6 +44,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
// passport oauth strategy for Google
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
if (isGoogleOauthActive) {
@@ -52,8 +55,9 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string,
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`,
scope: ["profile", " email"],
state: true
scope: ["profile", "email"],
state: true,
pkce: true
},
// eslint-disable-next-line
async (req, _accessToken, _refreshToken, profile, cb) => {
@@ -89,34 +93,44 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
const isGithubOauthActive = Boolean(appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITHUB_LOGIN);
if (isGithubOauthActive) {
passport.use(
new GitHubStrategy(
"github",
new OAuth2Strategy(
{
passReqToCallback: true,
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string,
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string,
authorizationURL: "https://github.com/login/oauth/authorize",
tokenURL: "https://github.com/login/oauth/access_token",
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN!,
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN!,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`,
scope: ["user:email"],
// akhilmhdh: because the ts type for this is outdated by the maintainer
state: true as unknown as string
scope: ["user:email", "read:org"],
state: true,
pkce: true,
passReqToCallback: true
},
// eslint-disable-next-line
async (req, accessToken, _refreshToken, profile, cb) => {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
try {
const ghEmails = await fetchGithubEmails(accessToken);
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
if (!email) throw new Error("No primary email found");
// profile does not get automatically populated so we need to manually fetch user info
const user = await fetchGithubUser(accessToken);
const callbackPort = req.session.get("callbackPort");
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
email,
firstName: profile.displayName || profile.username || "",
firstName: user.name || user.login,
lastName: "",
authMethod: AuthMethod.GITHUB,
callbackPort
});
return cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
logger.error(error);
cb(error as Error, false);
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
} catch (err) {
logger.error(err);
done(err as Error, false);
}
}
)
@@ -136,7 +150,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`,
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL,
state: true
state: true,
pkce: true
},
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
try {
@@ -166,17 +181,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
method: "GET",
schema: {
querystring: z.object({
callback_port: z.string().optional()
callback_port: z.string().optional(),
is_admin_login: z
.string()
.optional()
.transform((val) => val === "true")
})
},
preValidation: [
async (req, res) => {
const { callback_port: callbackPort } = req.query;
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
if (isAdminLogin) {
req.session.set("isAdminLogin", isAdminLogin);
}
return (
passport.authenticate("google", {
scope: ["profile", "email"],
@@ -200,10 +222,13 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// this is due to zod type difference
}) as never,
handler: async (req, res) => {
const isAdminLogin = req.session.get("isAdminLogin");
await req.session.destroy();
if (req.passportUser.isUserCompleted) {
return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
isAdminLogin ? `&isAdminLogin=${isAdminLogin}` : ""
}`
);
}
return res.redirect(
@@ -217,18 +242,26 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
method: "GET",
schema: {
querystring: z.object({
callback_port: z.string().optional()
callback_port: z.string().optional(),
is_admin_login: z
.string()
.optional()
.transform((val) => val === "true")
})
},
preValidation: [
async (req, res) => {
const { callback_port: callbackPort } = req.query;
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
if (isAdminLogin) {
req.session.set("isAdminLogin", isAdminLogin);
}
return (
passport.authenticate("github", {
session: false,
@@ -289,10 +322,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// this is due to zod type difference
}) as any,
handler: async (req, res) => {
const isAdminLogin = req.session.get("isAdminLogin");
await req.session.destroy();
if (req.passportUser.externalProviderAccessToken) {
void res.cookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, req.passportUser.externalProviderAccessToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
expires: new Date(Date.now() + ms(appCfg.JWT_PROVIDER_AUTH_LIFETIME))
});
}
if (req.passportUser.isUserCompleted) {
return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
isAdminLogin ? `&isAdminLogin=${isAdminLogin}` : ""
}`
);
}
return res.redirect(
@@ -306,18 +353,26 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
method: "GET",
schema: {
querystring: z.object({
callback_port: z.string().optional()
callback_port: z.string().optional(),
is_admin_login: z
.string()
.optional()
.transform((val) => val === "true")
})
},
preValidation: [
async (req, res) => {
const { callback_port: callbackPort } = req.query;
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
if (isAdminLogin) {
req.session.set("isAdminLogin", isAdminLogin);
}
return (
passport.authenticate("gitlab", {
session: false,
@@ -342,10 +397,13 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
handler: async (req, res) => {
const isAdminLogin = req.session.get("isAdminLogin");
await req.session.destroy();
if (req.passportUser.isUserCompleted) {
return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
isAdminLogin ? `&isAdminLogin=${isAdminLogin}` : ""
}`
);
}
return res.redirect(

View File

@@ -252,6 +252,31 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "DELETE",
url: "/me/sessions/:sessionId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
sessionId: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.authToken.revokeMySessionById(req.permission.id, req.params.sessionId);
return {
message: "Successfully revoked session"
};
}
});
server.route({
method: "GET",
url: "/me",

View File

@@ -1,5 +1,6 @@
import { z } from "zod";
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter";
@@ -70,6 +71,21 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
};
}
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
if (githubOauthAccessToken) {
await server.services.githubOrgSync
.syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken)
.finally(() => {
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: cfg.HTTPS_ENABLED,
maxAge: 0
});
});
}
void res.setCookie("jid", tokens.refresh, {
httpOnly: true,
path: "/",
@@ -77,6 +93,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: cfg.HTTPS_ENABLED,
maxAge: 0
});
return { token: tokens.access, isMfaEnabled: false };
}
});
@@ -131,6 +155,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return {
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,

View File

@@ -662,6 +662,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.optional()
.nullable()
.describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays),
secretReminderRecipients: z.string().array().optional().describe(RAW_SECRETS.UPDATE.secretReminderRecipients),
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment)
}),
@@ -692,6 +693,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
skipMultilineEncoding: req.body.skipMultilineEncoding,
tagIds: req.body.tagIds,
secretReminderRepeatDays: req.body.secretReminderRepeatDays,
secretReminderRecipients: req.body.secretReminderRecipients,
secretReminderNote: req.body.secretReminderNote,
metadata: req.body.metadata,
newSecretName: req.body.newSecretName,

View File

@@ -12,7 +12,9 @@ export enum AppConnection {
MsSql = "mssql",
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0"
Auth0 = "auth0",
LDAP = "ldap",
TeamCity = "teamcity"
}
export enum AWSRegion {

View File

@@ -41,8 +41,14 @@ import {
HumanitecConnectionMethod,
validateHumanitecConnectionCredentials
} from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import {
getTeamCityConnectionListItem,
TeamCityConnectionMethod,
validateTeamCityConnectionCredentials
} from "./teamcity";
import {
getTerraformCloudConnectionListItem,
TerraformCloudConnectionMethod,
@@ -71,7 +77,9 @@ export const listAppConnectionOptions = () => {
getMsSqlConnectionListItem(),
getCamundaConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem()
getAuth0ConnectionListItem(),
getLdapConnectionListItem(),
getTeamCityConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -135,7 +143,9 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -167,9 +177,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case MsSqlConnectionMethod.UsernameAndPassword:
return "Username & Password";
case WindmillConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken:
return "Access Token";
case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials";
case LdapConnectionMethod.SimpleBind:
return "Simple Bind";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -214,5 +227,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
};

View File

@@ -14,5 +14,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.MsSql]: "Microsoft SQL Server",
[AppConnection.Camunda]: "Camunda",
[AppConnection.Windmill]: "Windmill",
[AppConnection.Auth0]: "Auth0"
[AppConnection.Auth0]: "Auth0",
[AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity"
};

View File

@@ -43,8 +43,11 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
@@ -74,7 +77,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -450,6 +455,7 @@ export const appConnectionServiceFactory = ({
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
teamcity: teamcityConnectionService(connectAppConnectionById)
};
};

View File

@@ -57,12 +57,24 @@ import {
THumanitecConnectionInput,
TValidateHumanitecConnectionCredentialsSchema
} from "./humanitec";
import {
TLdapConnection,
TLdapConnectionConfig,
TLdapConnectionInput,
TValidateLdapConnectionCredentialsSchema
} from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TPostgresConnection,
TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema
} from "./postgres";
import {
TTeamCityConnection,
TTeamCityConnectionConfig,
TTeamCityConnectionInput,
TValidateTeamCityConnectionCredentialsSchema
} from "./teamcity";
import {
TTerraformCloudConnection,
TTerraformCloudConnectionConfig,
@@ -97,6 +109,8 @@ export type TAppConnection = { id: string } & (
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
| TLdapConnection
| TTeamCityConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -118,6 +132,8 @@ export type TAppConnectionInput = { id: string } & (
| TCamundaConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput
| TLdapConnectionInput
| TTeamCityConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -144,7 +160,9 @@ export type TAppConnectionConfig =
| TSqlConnectionConfig
| TCamundaConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig;
| TAuth0ConnectionConfig
| TLdapConnectionConfig
| TTeamCityConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -160,7 +178,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema;
| TValidateAuth0ConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -168,6 +188,10 @@ export type TListAwsConnectionKmsKeys = {
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
};
export type TListAwsConnectionIamUsers = {
connectionId: string;
};
export type TAppConnectionCredentialsValidator = (
appConnection: TAppConnectionConfig
) => Promise<TAppConnection["credentials"]>;

View File

@@ -1,9 +1,11 @@
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import AWS from "aws-sdk";
import { AxiosError } from "axios";
import { randomUUID } from "crypto";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { AwsConnectionMethod } from "./aws-connection-enums";
@@ -90,9 +92,20 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
const sts = new AWS.STS(awsConfig);
resp = await sts.getCallerIdentity().promise();
} catch (e: unknown) {
} catch (error: unknown) {
logger.error(error, "Error validating AWS connection credentials");
let message: string;
if (error instanceof AxiosError) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message = (error.response?.data?.message as string) || error.message || "verify credentials";
} else {
message = (error as Error)?.message || "verify credentials";
}
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
message: `Unable to validate connection: ${message}`
});
}

View File

@@ -2,7 +2,10 @@ import AWS from "aws-sdk";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
import {
TListAwsConnectionIamUsers,
TListAwsConnectionKmsKeys
} from "@app/services/app-connection/app-connection-types";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -70,6 +73,23 @@ const listAwsKmsKeys = async (
return kmsKeys;
};
const listAwsIamUsers = async (appConnection: TAwsConnection) => {
const { credentials } = await getAwsConnectionConfig(appConnection);
const iam = new AWS.IAM({ credentials });
const userEntries: AWS.IAM.User[] = [];
let userMarker: string | undefined;
do {
// eslint-disable-next-line no-await-in-loop
const response = await iam.listUsers({ MaxItems: 100, Marker: userMarker }).promise();
userEntries.push(...(response.Users || []));
userMarker = response.Marker;
} while (userMarker);
return userEntries;
};
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listKmsKeys = async (
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
@@ -82,7 +102,16 @@ export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
return kmsKeys;
};
const listIamUsers = async ({ connectionId }: TListAwsConnectionIamUsers, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
const iamUsers = await listAwsIamUsers(appConnection);
return iamUsers;
};
return {
listKmsKeys
listKmsKeys,
listIamUsers
};
};

View File

@@ -0,0 +1,4 @@
export * from "./ldap-connection-enums";
export * from "./ldap-connection-fns";
export * from "./ldap-connection-schemas";
export * from "./ldap-connection-types";

View File

@@ -0,0 +1,7 @@
export enum LdapConnectionMethod {
SimpleBind = "simple-bind"
}
export enum LdapProvider {
ActiveDirectory = "active-directory"
}

View File

@@ -0,0 +1,102 @@
import ldap from "ldapjs";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types";
export const getLdapConnectionListItem = () => {
return {
name: "LDAP" as const,
app: AppConnection.LDAP as const,
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
};
};
const LDAP_TIMEOUT = 15_000;
export const getLdapConnectionClient = async ({
url,
dn,
password,
sslCertificate,
sslRejectUnauthorized = true
}: TLdapConnectionConfig["credentials"]) => {
await blockLocalAndPrivateIpAddresses(url);
const isSSL = url.startsWith("ldaps");
return new Promise<ldap.Client>((resolve, reject) => {
const client = ldap.createClient({
url,
timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined
}
: undefined
});
client.on("error", (err: Error) => {
logger.error(err, "LDAP Error");
client.destroy();
reject(new Error(`Provider Error - ${err.message}`));
});
client.on("connectError", (err: Error) => {
logger.error(err, "LDAP Connection Error");
client.destroy();
reject(new Error(`Provider Connect Error - ${err.message}`));
});
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
reject(new Error(`Bind Error: ${err.message}`));
client.destroy();
}
resolve(client);
});
});
});
};
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
let client: ldap.Client | undefined;
try {
client = await getLdapConnectionClient(credentials);
// this shouldn't occur as handle connection error events in client but here as fallback
if (!client.connected) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
}
return credentials;
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
});
} finally {
client?.destroy();
}
};

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