Compare commits

..

106 Commits

Author SHA1 Message Date
Maidul Islam
aabf933756 Add bug bounty program
Added a formal bounty program
2025-04-30 18:56:23 -04:00
Maidul Islam
5d44d58ff4 update postgres reqs 2025-04-30 17:53:41 -04:00
carlosmonastyrski
ff294dab8d Merge pull request #3507 from Infisical/feat/orgUserAuthTokenExpiration
feat(user-auth): make users auth token expiration customizable for orgs
2025-04-30 18:18:38 -03:00
carlosmonastyrski
c99440ba81 feat(user-auth): use ms library and update docs 2025-04-30 16:49:33 -03:00
carlosmonastyrski
6d5a6f42e0 Merge branch 'main' into feat/orgUserAuthTokenExpiration 2025-04-30 15:59:52 -03:00
x032205
0c027fdc43 Merge pull request #3516 from Infisical/feat/teamcity-root-project
remove _Root filter for projects
2025-04-30 12:07:24 -04:00
x
727a6a7701 remove _Root filter for projects 2025-04-30 10:31:40 -04:00
carlosmonastyrski
7f1f9e7fd0 Merge pull request #3491 from Infisical/feat/improveSecretReferenceWarning
feat(secrets-ui): Add direct reference warning on secrets updates and add secret sync warning on deletion
2025-04-30 08:17:55 -03:00
Daniel Hougaard
98f742a807 Merge pull request #3513 from Infisical/daniel/k8s-hsm-docs
docs: fix hsm kubernetes documentation
2025-04-30 06:10:30 +04:00
Daniel Hougaard
66f1967f88 Update hsm-integration.mdx 2025-04-30 05:37:55 +04:00
Daniel Hougaard
da6cf85c8d fix: remove log output file 2025-04-30 05:37:07 +04:00
Daniel Hougaard
e8b6eb0573 docs: fix hsm kubernetes documentation 2025-04-30 05:09:39 +04:00
Maidul Islam
03ad5c5db0 Merge pull request #3512 from Infisical/daniel/kms-docs
docs: prerequisite for aws key
2025-04-29 20:39:30 -04:00
Daniel Hougaard
e6c4c27a87 docs: added pre-req for aws key 2025-04-30 03:36:07 +04:00
Daniel Hougaard
d4ac4f8d8f Update CollapsibleSecretImports.tsx 2025-04-30 03:13:10 +04:00
carlosmonastyrski
f0229c5ecf feat(user-auth): fix migration bug for e2e suite 2025-04-29 18:48:08 -03:00
carlosmonastyrski
8d711af23b feat(secrets-ui): change secret sync icon color 2025-04-29 18:39:41 -03:00
carlosmonastyrski
7bd61d88fc feat(user-auth): improve token refresh logic and default values 2025-04-29 18:28:18 -03:00
Scott Wilson
ba94b91974 Merge pull request #3510 from Infisical/internal-ip-check-fix
fix(external-connections): Use Hostname for Blocking Internal IPs DNS Resolve
2025-04-29 12:37:46 -07:00
Scott Wilson
b65f62fda8 fix: use hostname for blocking internal IPs 2025-04-29 12:26:29 -07:00
carlosmonastyrski
c47d76a6c7 feat(secrets-ui): improve warning message table 2025-04-29 14:19:52 -03:00
x032205
9138a9e71d Merge pull request #3509 from Infisical/feat/teamcity-ignore-inherited-secrets
feat(secret-sync): TeamCity ignore inherited and non-env values
2025-04-29 12:49:01 -04:00
x
8e4ad8baf8 docs tweak 2025-04-29 12:43:44 -04:00
x
9f158d5b3f feat(docs): Added note stating that inherited secrets are ignored 2025-04-29 10:35:56 -04:00
x
0e1cb4ebb2 Merge branch 'main' into feat/teamcity-ignore-inherited-secrets 2025-04-29 10:31:51 -04:00
carlosmonastyrski
e959ed7fab feat(secrets-ui): improve warning message and logic for secret-sync on secret imports 2025-04-29 10:15:53 -03:00
carlosmonastyrski
4e4b1b689b Merge branch 'main' into feat/improveSecretReferenceWarning 2025-04-29 08:43:35 -03:00
Maidul Islam
8f07f43fbd Merge pull request #3504 from akhilmhdh/doc/assume-privilege
doc: added doc for assume privilege feature
2025-04-28 20:08:44 -07:00
Maidul Islam
023f5d1286 revise docs 2025-04-28 23:06:37 -04:00
Daniel Hougaard
72b03d4bdf Merge pull request #3506 from Infisical/daniel/build-strict-find-filter
feat: strict find filter
2025-04-29 05:41:39 +04:00
Daniel Hougaard
e870e35002 consolidated filtering functions into one 2025-04-29 04:27:10 +04:00
carlosmonastyrski
4544f621af Merge pull request #3478 from Infisical/fix/UISecretEditPermissionButNotReadValuePermission
fix(secrets-table): UI fix for users with edit permissions but not read secret value permission
2025-04-28 20:23:34 -03:00
x
ddb5098eda only sync non-inherited environment variables 2025-04-28 19:09:13 -04:00
carlosmonastyrski
35749e8d12 feat(user-auth): allow edit overwritter rotation value on overview table 2025-04-28 20:02:50 -03:00
carlosmonastyrski
024ed0c0d8 feat(user-auth): add pr suggestions 2025-04-28 18:19:44 -03:00
carlosmonastyrski
e99e360339 feat(user-auth): make users auth token expiration customizable for orgs 2025-04-28 17:43:10 -03:00
Daniel Hougaard
85965184f8 Update secret-v2-bridge-dal.ts 2025-04-29 00:18:13 +04:00
Daniel Hougaard
a1bbd50c0b feat: build strict find filter 2025-04-29 00:09:30 +04:00
carlosmonastyrski
f9c936865a feat(secrets-ui): minor improvements from PR suggestions 2025-04-28 16:49:29 -03:00
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
=
6bca854475 doc: added doc for assume privilege feature 2025-04-29 00:12:37 +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
carlosmonastyrski
4fa7ba2ec7 Merge branch 'main' into fix/UISecretEditPermissionButNotReadValuePermission 2025-04-28 13:33:05 -03: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
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
carlosmonastyrski
c229d6888c feat(secrets-ui): allow read access to personal overrides 2025-04-25 20:41:44 -03: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
carlosmonastyrski
f345801bd6 feat(secrets-ui): improve types and code quality 2025-04-25 18:17:33 -03:00
carlosmonastyrski
f460acf9b4 fix(secrets-permissions): Fix case for rotated secrets 2025-04-25 17:56:56 -03:00
carlosmonastyrski
4160009913 feat(secrets-ui): add direct reference warning on secrets updates 2025-04-25 17:38:43 -03:00
carlosmonastyrski
d5065af7e9 feat(secrets-ui): add secret syncs to referenced secret warning 2025-04-25 15:26:34 -03:00
carlosmonastyrski
3de5fa066b fix(secrets-permissions): Fix setTimeout and eye icon size 2025-04-25 08:54:25 -03:00
carlosmonastyrski
b377d2a6b1 fix(secrets-permissions): Fix setTimeout 2025-04-24 11:15:42 -03:00
carlosmonastyrski
350272aa57 fix(secrets-permissions): UI improvements 2025-04-24 08:10:10 -03:00
carlosmonastyrski
95489e1b0a fix(secrets-permissions): UI improvements 2025-04-23 22:24:41 -03:00
carlosmonastyrski
56b3e7a76d fix(secrets-permissions): UI fix for users with edit permissions but not read secret value permission 2025-04-23 21:09:19 -03:00
Daniel Hougaard
9ea6eca560 requested changes 2025-04-23 21:40:01 +04: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
Sheen Capadngan
75f1ce7b86 feat: enabled sso to bypass org sso 2025-04-23 02:28:58 +08: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
180 changed files with 4369 additions and 688 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,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

@@ -0,0 +1,27 @@
import { Knex } from "knex";
import { getConfig } from "@app/lib/config/env";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const appCfg = getConfig();
const tokenDuration = appCfg?.JWT_REFRESH_LIFETIME;
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.string("userTokenExpiration");
});
if (tokenDuration) {
await knex(TableName.Organization).update({ userTokenExpiration: tokenDuration });
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration")) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.dropColumn("userTokenExpiration");
});
}
}

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,11 +23,13 @@ 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(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
bypassOrgAuthEnabled: z.boolean().default(false)
bypassOrgAuthEnabled: z.boolean().default(false),
userTokenExpiration: z.string().nullable().optional()
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

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

@@ -320,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[] = [
@@ -1494,6 +1496,7 @@ interface CreateSshHost {
metadata: {
sshHostId: string;
hostname: string;
alias: string | null;
userCertTtl: string;
hostCertTtl: string;
loginMappings: {
@@ -1512,6 +1515,7 @@ interface UpdateSshHost {
metadata: {
sshHostId: string;
hostname?: string;
alias?: string | null;
userCertTtl?: string;
hostCertTtl?: string;
loginMappings?: {
@@ -2452,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: {
@@ -2757,6 +2784,8 @@ export type Event =
| KmipOperationLocateEvent
| KmipOperationRegisterEvent
| ProjectAccessRequestEvent
| ProjectAssumePrivilegesEvent
| ProjectAssumePrivilegesExitEvent
| CreateSecretRequestEvent
| SecretApprovalRequestReview
| GetSecretRotationsEvent

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

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

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

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

@@ -6,4 +6,5 @@ export * from "./array";
export * from "./dates";
export * from "./object";
export * from "./string";
export * from "./time";
export * from "./undefined";

View File

@@ -0,0 +1,21 @@
import ms, { StringValue } from "ms";
const convertToMilliseconds = (exp: string | number): number => {
if (typeof exp === "number") {
return exp * 1000;
}
const result = ms(exp as StringValue);
if (typeof result !== "number") {
throw new Error(`Invalid expiration format: ${exp}`);
}
return result;
};
export const getMinExpiresIn = (exp1: string | number, exp2: string | number): string | number => {
const ms1 = convertToMilliseconds(exp1);
const ms2 = convertToMilliseconds(exp2);
return ms1 <= ms2 ? exp1 : exp2;
};

View File

@@ -2,6 +2,8 @@
import { Knex } from "knex";
import { Tables } from "knex/types/tables";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "../errors";
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
@@ -25,28 +27,41 @@ export type TFindFilter<R extends object = object> = Partial<R> & {
$search?: Partial<{ [k in keyof R]: R[k] }>;
$complex?: TKnexDynamicOperator<R>;
};
export const buildFindFilter =
<R extends object = object>({ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>) =>
<R extends object = object>(
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
tableName?: TableName,
excludeKeys?: Array<keyof R>
) =>
(bd: Knex.QueryBuilder<R, R>) => {
void bd.where(filter);
const processedFilter = tableName
? Object.fromEntries(
Object.entries(filter)
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R))
.map(([key, value]) => [`${tableName}.${key}`, value])
)
: filter;
void bd.where(processedFilter);
if ($in) {
Object.entries($in).forEach(([key, val]) => {
if (val) {
void bd.whereIn(key as never, val as never);
void bd.whereIn([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
}
});
}
if ($notNull?.length) {
$notNull.forEach((key) => {
void bd.whereNotNull(key as never);
void bd.whereNotNull([`${tableName ? `${tableName}.` : ""}${key as string}`] as never);
});
}
if ($search) {
Object.entries($search).forEach(([key, val]) => {
if (val) {
void bd.whereILike(key as never, val as never);
void bd.whereILike([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
}
});
}

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

@@ -15,13 +15,13 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
const validUrl = new URL(url);
const inputHostIps: string[] = [];
if (isIPv4(validUrl.host)) {
inputHostIps.push(validUrl.host);
if (isIPv4(validUrl.hostname)) {
inputHostIps.push(validUrl.hostname);
} else {
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
if (validUrl.hostname === "localhost" || validUrl.hostname === "host.docker.internal") {
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
}
const resolvedIps = await dns.resolve4(validUrl.host);
const resolvedIps = await dns.resolve4(validUrl.hostname);
inputHostIps.push(...resolvedIps);
}
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));

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,
@@ -728,6 +749,7 @@ export const registerRoutes = async (
projectKeyDAL,
projectRoleDAL,
groupProjectDAL,
secretReminderRecipientsDAL,
licenseService
});
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
@@ -961,6 +983,7 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
projectKeyDAL,
projectUserMembershipRoleDAL,
secretReminderRecipientsDAL,
orgService,
resourceMetadataDAL,
secretSyncQueue
@@ -1022,7 +1045,9 @@ export const registerRoutes = async (
projectRoleDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
projectDAL
projectDAL,
identityDAL,
userDAL
});
const snapshotService = secretSnapshotServiceFactory({
@@ -1516,6 +1541,7 @@ export const registerRoutes = async (
const secretSyncService = secretSyncServiceFactory({
secretSyncDAL,
secretImportDAL,
permissionService,
appConnectionService,
folderDAL,
@@ -1675,7 +1701,9 @@ export const registerRoutes = async (
kmip: kmipService,
kmipOperation: kmipOperationService,
gateway: gatewayService,
secretRotationV2: secretRotationV2Service
secretRotationV2: secretRotationV2Service,
assumePrivileges: assumePrivilegeService,
githubOrgSync: githubOrgSyncConfigService
});
const cronJobs: CronJob[] = [];
@@ -1696,6 +1724,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);
@@ -1735,30 +1764,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

@@ -2,6 +2,7 @@ import jwt from "jsonwebtoken";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { getMinExpiresIn } from "@app/lib/fn";
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
@@ -33,6 +34,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" };
}
});
@@ -71,6 +80,18 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
if (decodedToken.organizationId) {
const org = await server.services.org.findOrganizationById(
decodedToken.userId,
decodedToken.organizationId,
decodedToken.authMethod,
decodedToken.organizationId
);
if (org && org.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
}
}
const token = jwt.sign(
{
@@ -84,7 +105,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
mfaMethod: decodedToken.mfaMethod
},
appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
{ expiresIn }
);
return { token, organizationId: decodedToken.organizationId };

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";
@@ -154,7 +154,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: z
.object({
secretId: z.string(),
referencedSecretKey: z.string()
referencedSecretKey: z.string(),
referencedSecretEnv: z.string()
})
.array()
.optional()
@@ -166,6 +167,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
})
.array()
.optional(),
usedBySecretSyncs: z
.object({
name: z.string(),
destination: z.string(),
environment: z.string(),
id: z.string(),
path: z.string()
})
.array()
.optional(),
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
@@ -500,6 +511,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
const usedBySecretSyncs: { name: string; destination: string; environment: string; id: string; path: string }[] =
[];
for await (const environment of environments) {
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
{ projectId, secretPath, environment },
req.permission
);
secretSyncs.forEach((sync) => {
usedBySecretSyncs.push({
name: sync.name,
destination: sync.destination,
environment,
id: sync.id,
path: sync.folder?.path || "/"
});
});
}
return {
folders,
dynamicSecrets,
@@ -512,6 +541,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount,
totalSecretRotationCount,
importedByEnvs,
usedBySecretSyncs,
totalCount:
(totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) +
@@ -594,6 +624,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(),
@@ -605,6 +641,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
usedBySecretSyncs: z
.object({
name: z.string(),
destination: z.string(),
environment: z.string(),
id: z.string(),
path: z.string()
})
.array()
.optional(),
importedBy: z
.object({
environment: z.object({
@@ -618,7 +664,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: z
.object({
secretId: z.string(),
referencedSecretKey: z.string()
referencedSecretKey: z.string(),
referencedSecretEnv: z.string()
})
.array()
.optional()
@@ -898,6 +945,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets
});
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
{ projectId, secretPath, environment },
req.permission
);
const usedBySecretSyncs = secretSyncs.map((sync) => ({
name: sync.name,
destination: sync.destination,
environment: sync.environment?.name || environment,
id: sync.id,
path: sync.folder?.path || "/"
}));
if (secrets?.length || secretRotations?.length) {
const secretCount =
(secrets?.length ?? 0) +
@@ -944,6 +1003,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount,
totalSecretRotationCount,
importedBy,
usedBySecretSyncs,
totalCount:
(totalImportCount ?? 0) +
(totalFolderCount ?? 0) +

View File

@@ -1,3 +1,4 @@
import RE2 from "re2";
import { z } from "zod";
import {
@@ -263,7 +264,18 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
allowSecretSharingOutsideOrganization: z.boolean().optional(),
bypassOrgAuthEnabled: z.boolean().optional()
bypassOrgAuthEnabled: z.boolean().optional(),
userTokenExpiration: z
.string()
.refine((val) => new RE2(/^\d+[mhdw]$/).test(val), "Must be a number followed by m, h, d, or w")
.refine(
(val) => {
const numericPart = val.slice(0, -1);
return parseInt(numericPart, 10) >= 1;
},
{ message: "Duration value must be at least 1" }
)
.optional()
}),
response: {
200: z.object({

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

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

@@ -69,6 +69,5 @@ export const listTeamCityProjects = async (appConnection: TTeamCityConnection) =
}
);
// Filter out the root project. Should not be seen by users.
return resp.data.project.filter((proj) => proj.id !== "_Root");
return resp.data.project;
};

View File

@@ -12,7 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -143,6 +143,17 @@ export const authLoginServiceFactory = ({
);
if (!tokenSession) throw new Error("Failed to create token");
let tokenSessionExpiresIn: string | number = cfg.JWT_AUTH_LIFETIME;
let refreshTokenExpiresIn: string | number = cfg.JWT_REFRESH_LIFETIME;
if (organizationId) {
const org = await orgDAL.findById(organizationId);
if (org && org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = org.userTokenExpiration;
}
}
const accessToken = jwt.sign(
{
authMethod,
@@ -155,7 +166,7 @@ export const authLoginServiceFactory = ({
mfaMethod
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
{ expiresIn: tokenSessionExpiresIn }
);
const refreshToken = jwt.sign(
@@ -170,7 +181,7 @@ export const authLoginServiceFactory = ({
mfaMethod
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
{ expiresIn: refreshTokenExpiresIn }
);
return { access: accessToken, refresh: refreshToken };
@@ -476,6 +487,7 @@ export const authLoginServiceFactory = ({
return {
...tokens,
user,
isMfaEnabled: false
};
};
@@ -784,7 +796,7 @@ export const authLoginServiceFactory = ({
organizationId
});
return { token, isMfaEnabled: false, user: userEnc } as const;
return { token, isMfaEnabled: false, user: userEnc, decodedProviderToken } as const;
};
/*

View File

@@ -10,6 +10,7 @@ import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { getMinExpiresIn } from "@app/lib/fn";
import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -46,7 +47,7 @@ type TAuthSignupDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
@@ -320,6 +321,17 @@ export const authSignupServiceFactory = ({
projectBotDAL
});
let tokenSessionExpiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
if (organizationId) {
const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId);
if (org && org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = org.userTokenExpiration;
}
}
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
@@ -337,7 +349,7 @@ export const authSignupServiceFactory = ({
organizationId
},
appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
{ expiresIn: tokenSessionExpiresIn }
);
const refreshToken = jwt.sign(
@@ -350,7 +362,7 @@ export const authSignupServiceFactory = ({
organizationId
},
appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
{ expiresIn: refreshTokenExpiresIn }
);
return { user: updateduser.info, accessToken, refreshToken, organizationId };

View File

@@ -177,6 +177,7 @@ export const deleteGithubSecrets = async ({
selected_repositories_url?: string | undefined;
}
// @ts-expect-error just octokit ts compatiability issue
const OctokitWithRetry = Octokit.plugin(retry);
let octokit: Octokit;
const appCfg = getConfig();

View File

@@ -342,9 +342,12 @@ export const kmsServiceFactory = ({
}
return async ({ cipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const { data } = await externalKms.decrypt(cipherTextBlob);
return data;
try {
const { data } = await externalKms.decrypt(cipherTextBlob);
return data;
} finally {
await externalKms.cleanup();
}
};
}
@@ -557,9 +560,12 @@ export const kmsServiceFactory = ({
}
return async ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const { encryptedBlob } = await externalKms.encrypt(plainText);
return { cipherTextBlob: encryptedBlob };
try {
const { encryptedBlob } = await externalKms.encrypt(plainText);
return { cipherTextBlob: encryptedBlob };
} finally {
await externalKms.cleanup();
}
};
}

View File

@@ -17,5 +17,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
shouldUseNewPrivilegeSystem: true,
privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true,
bypassOrgAuthEnabled: true
bypassOrgAuthEnabled: true,
userTokenExpiration: true
});

View File

@@ -170,8 +170,12 @@ export const orgServiceFactory = ({
actorOrgId: string | undefined
) => {
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
const appCfg = getConfig();
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
if (!org.userTokenExpiration) {
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
}
return org;
};
/*
@@ -350,7 +354,8 @@ export const orgServiceFactory = ({
enforceMfa,
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled
bypassOrgAuthEnabled,
userTokenExpiration
}
}: TUpdateOrgDTO) => {
const appCfg = getConfig();
@@ -451,7 +456,8 @@ export const orgServiceFactory = ({
enforceMfa,
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled
bypassOrgAuthEnabled,
userTokenExpiration
});
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org;

View File

@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
selectedMfaMethod: MfaMethod;
allowSecretSharingOutsideOrganization: boolean;
bypassOrgAuthEnabled: boolean;
userTokenExpiration: string;
}>;
} & TOrgPermission;

View File

@@ -23,6 +23,7 @@ import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
@@ -53,6 +54,7 @@ type TProjectMembershipServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
groupProjectDAL: TGroupProjectDALFactory;
};
@@ -71,6 +73,7 @@ export const projectMembershipServiceFactory = ({
groupProjectDAL,
projectDAL,
projectKeyDAL,
secretReminderRecipientsDAL,
licenseService
}: TProjectMembershipServiceFactoryDep) => {
const getProjectMemberships = async ({
@@ -389,6 +392,13 @@ export const projectMembershipServiceFactory = ({
const membership = await projectMembershipDAL.transaction(async (tx) => {
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
await secretReminderRecipientsDAL.delete(
{
projectId,
userId: deletedMembership.userId
},
tx
);
return deletedMembership;
});
return membership;
@@ -466,6 +476,16 @@ export const projectMembershipServiceFactory = ({
tx
);
await secretReminderRecipientsDAL.delete(
{
projectId,
$in: {
userId: projectMembers.map(({ user }) => user.id)
}
},
tx
);
// delete project keys belonging to users that are not part of any other groups in the project
await projectKeyDAL.delete(
{
@@ -526,6 +546,15 @@ export const projectMembershipServiceFactory = ({
},
tx
);
await secretReminderRecipientsDAL.delete(
{
projectId,
userId: actorId
},
tx
);
const membership = (
await projectMembershipDAL.delete(
{

View File

@@ -1,5 +1,6 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -12,10 +13,12 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorAuthMethod } from "../auth/auth-type";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectRoleDALFactory } from "./project-role-dal";
import { getPredefinedRoles } from "./project-role-fns";
import {
@@ -29,6 +32,8 @@ import {
type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory;
identityDAL: Pick<TIdentityDALFactory, "findById">;
userDAL: Pick<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
@@ -47,7 +52,9 @@ export const projectRoleServiceFactory = ({
permissionService,
identityProjectMembershipRoleDAL,
projectUserMembershipRoleDAL,
projectDAL
projectDAL,
identityDAL,
userDAL
}: TProjectRoleServiceFactoryDep) => {
const createRole = async ({ data, actor, actorId, actorAuthMethod, actorOrgId, filter }: TCreateRoleDTO) => {
let projectId = "";
@@ -220,14 +227,42 @@ export const projectRoleServiceFactory = ({
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission, membership } = await permissionService.getUserProjectPermission({
userId,
const { permission, membership } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: userId,
projectId,
authMethod: actorAuthMethod,
userOrgId: actorOrgId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
return { permissions: packRules(permission.rules), membership };
// just to satisfy ts
if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" });
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === projectId;
const assumedPrivilegeDetails = isAssumingPrivilege
? {
actorId: assumedPrivilegeDetailsCtx?.actorId,
actorType: assumedPrivilegeDetailsCtx?.actorType,
actorName: "",
actorEmail: ""
}
: undefined;
if (assumedPrivilegeDetails?.actorType === ActorType.IDENTITY) {
const identityDetails = await identityDAL.findById(assumedPrivilegeDetails.actorId);
if (!identityDetails)
throw new NotFoundError({ message: `Identity with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = identityDetails.name;
} else if (assumedPrivilegeDetails?.actorType === ActorType.USER) {
const userDetails = await userDAL.findById(assumedPrivilegeDetails?.actorId);
if (!userDetails)
throw new NotFoundError({ message: `User with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`;
assumedPrivilegeDetails.actorEmail = userDetails?.email || "";
}
return { permissions: packRules(permission.rules), membership, assumedPrivilegeDetails };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };

View File

@@ -171,6 +171,19 @@ export const secretImportDALFactory = (db: TDbClient) => {
}
};
const getFolderImports = async (secretPath: string, environmentId: string, tx?: Knex) => {
try {
const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
.where({ importPath: secretPath, importEnv: environmentId })
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select(db.ref("id").withSchema(TableName.SecretFolder).as("folderId"));
return folderImports;
} catch (error) {
throw new DatabaseError({ error, name: "get secret imports" });
}
};
const getFolderIsImportedBy = async (
secretPath: string,
environmentId: string,
@@ -203,7 +216,8 @@ export const secretImportDALFactory = (db: TDbClient) => {
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey")
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey"),
db.ref("environment").withSchema(TableName.SecretReferenceV2).as("referencedSecretEnv")
);
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
@@ -214,13 +228,14 @@ export const secretImportDALFactory = (db: TDbClient) => {
}));
const secretResults = secretReferences.map(
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey, referencedSecretEnv }) => ({
envName,
envSlug,
secretId,
folderName,
folderId,
referencedSecretKey
referencedSecretKey,
referencedSecretEnv
})
);
@@ -235,6 +250,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
secrets: {
secretId: string;
referencedSecretKey: string;
referencedSecretEnv: string;
}[];
folderId: string;
folderImported: boolean;
@@ -264,7 +280,11 @@ export const secretImportDALFactory = (db: TDbClient) => {
if ("secretId" in item && item.secretId) {
updatedAcc[env].folders[folder].secrets = [
...updatedAcc[env].folders[folder].secrets,
{ secretId: item.secretId, referencedSecretKey: item.referencedSecretKey }
{
secretId: item.secretId,
referencedSecretKey: item.referencedSecretKey,
referencedSecretEnv: item.referencedSecretEnv
}
];
} else {
updatedAcc[env].folders[folder].folderImported = true;
@@ -309,6 +329,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
findLastImportPosition,
updateAllPosition,
getProjectImportCount,
getFolderIsImportedBy
getFolderIsImportedBy,
getFolderImports
};
};

View File

@@ -808,7 +808,7 @@ export const secretImportServiceFactory = ({
actorOrgId,
secrets
}: TGetSecretImportsDTO & {
secrets: { secretKey: string; secretValue: string }[] | undefined;
secrets: { secretKey: string; secretValue: string; id: string }[] | undefined;
}) => {
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -877,7 +877,8 @@ export const secretImportServiceFactory = ({
)
.map((otherSecret) => ({
secretId: secret.secretKey,
referencedSecretKey: otherSecret.secretKey
referencedSecretKey: otherSecret.secretKey,
referencedSecretEnv: environment
}));
}) || [];
if (locallyReferenced.length > 0) {

View File

@@ -56,11 +56,12 @@ export type FolderResult = {
export type SecretResult = {
secretId: string;
referencedSecretKey: string;
referencedSecretEnv: string;
} & FolderResult;
export type FolderInfo = {
folderName: string;
secrets?: { secretId: string; referencedSecretKey: string }[];
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
folderId: string;
folderImported: boolean;
envSlug?: string;

View File

@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretReminderRecipientsDALFactory = ReturnType<typeof secretReminderRecipientsDALFactory>;
export const secretReminderRecipientsDALFactory = (db: TDbClient) => {
const secretReminderRecipientsOrm = ormify(db, TableName.SecretReminderRecipients);
const findUsersBySecretId = async (secretId: string, tx?: Knex) => {
const res = await (tx || db.replicaNode())(TableName.SecretReminderRecipients)
.where({ secretId })
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Project, `${TableName.SecretReminderRecipients}.projectId`, `${TableName.Project}.id`)
.leftJoin(TableName.OrgMembership, (bd) => {
void bd
.on(`${TableName.OrgMembership}.userId`, "=", `${TableName.SecretReminderRecipients}.userId`)
.andOn(`${TableName.OrgMembership}.orgId`, "=", `${TableName.Project}.orgId`);
})
.where(`${TableName.OrgMembership}.isActive`, true)
.select(selectAllTableCols(TableName.SecretReminderRecipients))
.select(
db.ref("email").withSchema(TableName.Users).as("email"),
db.ref("username").withSchema(TableName.Users).as("username"),
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
db.ref("lastName").withSchema(TableName.Users).as("lastName")
);
return res;
};
return { ...secretReminderRecipientsOrm, findUsersBySecretId };
};

View File

@@ -0,0 +1,8 @@
export type TSecretReminderRecipient = {
user: {
id: string;
username: string;
email?: string | null;
};
id: string;
};

View File

@@ -23,6 +23,7 @@ import {
TDeleteSecretSyncDTO,
TFindSecretSyncByIdDTO,
TFindSecretSyncByNameDTO,
TListSecretSyncsByFolderId,
TListSecretSyncsByProjectId,
TSecretSync,
TTriggerSecretSyncImportSecretsByIdDTO,
@@ -31,12 +32,14 @@ import {
TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { TSecretSyncDALFactory } from "./secret-sync-dal";
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
type TSecretSyncServiceFactoryDep = {
secretSyncDAL: TSecretSyncDALFactory;
secretImportDAL: TSecretImportDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
@@ -53,6 +56,7 @@ export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFacto
export const secretSyncServiceFactory = ({
secretSyncDAL,
folderDAL,
secretImportDAL,
permissionService,
appConnectionService,
projectBotService,
@@ -85,6 +89,37 @@ export const secretSyncServiceFactory = ({
return secretSyncs as TSecretSync[];
};
const listSecretSyncsBySecretPath = async (
{ projectId, secretPath, environment }: TListSecretSyncsByFolderId,
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId
});
if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) {
return [];
}
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return [];
const folderImports = await secretImportDAL.getFolderImports(secretPath, folder.envId);
const secretSyncs = await secretSyncDAL.find({
$in: {
folderId: folderImports.map((folderImport) => folderImport.folderId).concat(folder.id)
}
});
return secretSyncs as TSecretSync[];
};
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
const secretSync = await secretSyncDAL.findById(syncId);
@@ -518,6 +553,7 @@ export const secretSyncServiceFactory = ({
return {
listSecretSyncOptions,
listSecretSyncsByProjectId,
listSecretSyncsBySecretPath,
findSecretSyncById,
findSecretSyncByName,
createSecretSync,

View File

@@ -144,6 +144,13 @@ export type TListSecretSyncsByProjectId = {
destination?: SecretSync;
};
export type TListSecretSyncsByFolderId = {
projectId: string;
secretPath: string;
environment: string;
destination?: SecretSync;
};
export type TFindSecretSyncByIdDTO = {
syncId: string;
destination: SecretSync;

View File

@@ -10,7 +10,7 @@ import {
TTeamCitySyncWithCredentials
} from "@app/services/secret-sync/teamcity/teamcity-sync-types";
// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env.").
// Note: Most variables won't be returned with a value due to them being a "password" type.
// TeamCity API returns empty string for password-type variables for security reasons.
const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => {
const { data } = await request.get<TTeamCityListVariablesResponse>(
@@ -25,12 +25,16 @@ const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildC
}
);
// Filters for only non-inherited environment variables
// Strips out "env." from map key, but the "name" field still has the original unaltered key.
return Object.fromEntries(
data.property.map((variable) => [
variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name,
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
])
data.property
.filter((variable) => !variable.inherited)
.filter((variable) => variable.name.startsWith("env."))
.map((variable) => [
variable.name.substring(4),
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
])
);
};

View File

@@ -22,6 +22,7 @@ import type {
TFindSecretsByFolderIdsFilter,
TGetSecretsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { applyJitter } from "@app/lib/dates";
export const SecretServiceCacheKeys = {
get productKey() {
@@ -48,7 +49,7 @@ interface TSecretV2DalArg {
keyStore: TKeyStoreFactory;
}
export const SECRET_DAL_TTL = 5 * 60;
export const SECRET_DAL_TTL = () => applyJitter(10 * 60, 2 * 60);
export const SECRET_DAL_VERSION_TTL = 15 * 60;
export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
@@ -63,7 +64,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretV2)
.where(filter)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter, TableName.SecretV2))
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
@@ -79,7 +81,17 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
`${TableName.SecretV2}.id`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.leftJoin(
TableName.SecretReminderRecipients,
`${TableName.SecretV2}.id`,
`${TableName.SecretReminderRecipients}.secretId`
)
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId"))
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
@@ -103,6 +115,23 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
slug,
name: slug
})
},
{
key: "reminderRecipientId",
label: "secretReminderRecipients" as const,
mapper: ({
reminderRecipientId,
reminderRecipientUsername,
reminderRecipientEmail,
reminderRecipientUserId
}) => ({
user: {
id: reminderRecipientUserId,
username: reminderRecipientUsername,
email: reminderRecipientEmail
},
id: reminderRecipientId
})
}
]
});
@@ -484,6 +513,12 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(
TableName.SecretReminderRecipients,
`${TableName.SecretV2}.id`,
`${TableName.SecretReminderRecipients}.secretId`
)
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
@@ -512,6 +547,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
}) as rank`
)
)
.select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId"))
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
@@ -556,6 +595,23 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
isRotatedSecret: Boolean(el.rotationId)
}),
childrenMapper: [
{
key: "reminderRecipientId",
label: "secretReminderRecipients" as const,
mapper: ({
reminderRecipientId,
reminderRecipientUsername,
reminderRecipientEmail,
reminderRecipientUserId
}) => ({
user: {
id: reminderRecipientUserId,
username: reminderRecipientUsername,
email: reminderRecipientEmail
},
id: reminderRecipientId
})
},
{
key: "tagId",
label: "tags" as const,

View File

@@ -2,7 +2,7 @@ import path from "node:path";
import RE2 from "re2";
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
import { SecretType, TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
@@ -12,6 +12,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretReminderRecipient } from "../secret-reminder-recipients/secret-reminder-recipients-types";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@@ -353,7 +354,7 @@ export const fnSecretBulkDelete = async ({
deletedSecrets
.filter(({ reminderRepeatDays }) => Boolean(reminderRepeatDays))
.map(({ id, reminderRepeatDays }) =>
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: reminderRepeatDays as number })
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: reminderRepeatDays as number }, tx)
)
);
@@ -684,6 +685,7 @@ export const reshapeBridgeSecret = (
secretMetadata?: ResourceMetadataDTO;
isRotatedSecret?: boolean;
rotationId?: string;
secretReminderRecipients?: TSecretReminderRecipient[];
},
secretValueHidden: boolean
) => ({
@@ -715,9 +717,10 @@ export const reshapeBridgeSecret = (
updatedAt: secret.updatedAt,
isRotatedSecret: secret.isRotatedSecret,
rotationId: secret.rotationId,
secretReminderRecipients: secret.secretReminderRecipients || [],
...(secretValueHidden
? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValue: secret.type === SecretType.Personal ? secret.value : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: true
}
: {

View File

@@ -544,7 +544,12 @@ export const secretV2BridgeServiceFactory = ({
id: updatedSecret[0].id,
...inputSecret
},
oldSecret: secret,
oldSecret: {
id: secret.id,
secretReminderNote: secret.reminderNote,
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderRecipients: secret.secretReminderRecipients?.map((el) => el.user.id)
},
projectId
});
@@ -957,7 +962,7 @@ export const secretV2BridgeServiceFactory = ({
const encryptedCachedSecrets = await keyStore.getItem(cacheKey);
if (encryptedCachedSecrets) {
try {
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL());
const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") });
const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as {
secrets: typeof decryptedSecrets;
@@ -1127,7 +1132,7 @@ export const secretV2BridgeServiceFactory = ({
plainText: Buffer.from(JSON.stringify(payload))
}).cipherTextBlob;
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
}
return payload;
}
@@ -1174,7 +1179,7 @@ export const secretV2BridgeServiceFactory = ({
plainText: Buffer.from(JSON.stringify(payload))
}).cipherTextBlob;
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
}
return payload;
};

View File

@@ -94,6 +94,7 @@ export type TUpdateSecretDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretReminderRecipients?: string[] | null;
metadata?: {
source?: string;
};
@@ -220,7 +221,7 @@ export type TFnSecretBulkDelete = {
tx?: Knex;
secretDAL: Pick<TSecretV2BridgeDALFactory, "deleteMany">;
secretQueueService: {
removeSecretReminder: (data: TRemoveSecretReminderDTO) => Promise<void>;
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
};
};

View File

@@ -407,6 +407,7 @@ export const decryptSecretRaw = (
id: secret.id,
user: secret.userId,
tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
secretReminderRecipients: [],
skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote,
@@ -758,7 +759,7 @@ export const fnSecretBulkDelete = async ({
deletedSecrets
.filter(({ secretReminderRepeatDays }) => Boolean(secretReminderRepeatDays))
.map(({ id, secretReminderRepeatDays }) =>
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: secretReminderRepeatDays as number })
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: secretReminderRepeatDays as number }, tx)
)
);

View File

@@ -1,11 +1,13 @@
/* eslint-disable no-await-in-loop */
import opentelemetry from "@opentelemetry/api";
import { AxiosError } from "axios";
import { Knex } from "knex";
import {
ProjectMembershipRole,
ProjectUpgradeStatus,
ProjectVersion,
SecretType,
TSecretSnapshotSecretsV2,
TSecretVersionsV2
} from "@app/db/schemas";
@@ -53,6 +55,7 @@ import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-sche
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
@@ -109,6 +112,10 @@ type TSecretQueueFactoryDep = {
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretReminderRecipientsDAL: Pick<
TSecretReminderRecipientsDALFactory,
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
>;
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsSyncSecretsByPath">;
};
@@ -170,6 +177,7 @@ export const secretQueueFactory = ({
projectUserMembershipRoleDAL,
projectKeyDAL,
resourceMetadataDAL,
secretReminderRecipientsDAL,
secretSyncQueue
}: TSecretQueueFactoryDep) => {
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
@@ -178,7 +186,11 @@ export const secretQueueFactory = ({
unit: "1"
});
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const removeSecretReminder = async ({ deleteRecipients = true, ...dto }: TRemoveSecretReminderDTO, tx?: Knex) => {
if (deleteRecipients) {
await secretReminderRecipientsDAL.delete({ secretId: dto.secretId }, tx);
}
const appCfg = getConfig();
await queueService.stopRepeatableJob(
QueueName.SecretReminder,
@@ -224,7 +236,12 @@ export const secretQueueFactory = ({
.replace(":", "-");
};
const addSecretReminder = async ({ oldSecret, newSecret, projectId }: TCreateSecretReminderDTO) => {
const addSecretReminder = async ({
oldSecret,
newSecret,
projectId,
deleteRecipients = true
}: TCreateSecretReminderDTO) => {
try {
const appCfg = getConfig();
@@ -246,7 +263,8 @@ export const secretQueueFactory = ({
if (oldSecret.secretReminderRepeatDays) {
await removeSecretReminder({
repeatDays: oldSecret.secretReminderRepeatDays,
secretId: oldSecret.id
secretId: oldSecret.id,
deleteRecipients
});
}
@@ -283,29 +301,57 @@ export const secretQueueFactory = ({
};
const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => {
const { secretReminderRepeatDays, secretReminderNote } = newSecret;
const { secretReminderRepeatDays, secretReminderNote, secretReminderRecipients } = newSecret;
if (newSecret.type !== "personal" && secretReminderRepeatDays !== undefined) {
if (
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote)
) {
await addSecretReminder({
oldSecret,
newSecret,
projectId
});
} else if (
secretReminderRepeatDays === null &&
secretReminderNote === null &&
oldSecret.secretReminderRepeatDays
) {
await removeSecretReminder({
secretId: oldSecret.id,
repeatDays: oldSecret.secretReminderRepeatDays
});
const recipientsUpdated =
secretReminderRecipients?.some(
(newId) => !oldSecret.secretReminderRecipients?.find((oldId) => newId === oldId)
) || secretReminderRecipients?.length !== oldSecret.secretReminderRecipients?.length;
await secretReminderRecipientsDAL.transaction(async (tx) => {
if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) {
if (
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote)
) {
await addSecretReminder({
oldSecret,
newSecret,
projectId,
deleteRecipients: false
});
} else if (
secretReminderRepeatDays === null &&
secretReminderNote === null &&
oldSecret.secretReminderRepeatDays
) {
await removeSecretReminder({
secretId: oldSecret.id,
repeatDays: oldSecret.secretReminderRepeatDays
});
}
}
}
if (recipientsUpdated) {
// if no recipients, delete all existing recipients
if (!secretReminderRecipients?.length) {
const existingRecipients = await secretReminderRecipientsDAL.findUsersBySecretId(newSecret.id, tx);
if (existingRecipients) {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
}
} else {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
await secretReminderRecipientsDAL.insertMany(
secretReminderRecipients.map((r) => ({
secretId: newSecret.id,
userId: r,
projectId
})),
tx
);
}
}
});
};
const createManySecretsRawFn = createManySecretsRawFnFactory({
projectDAL,
@@ -1071,6 +1117,8 @@ export const secretQueueFactory = ({
const secret = await secretV2BridgeDAL.findById(data.secretId);
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
const recipients = await secretReminderRecipientsDAL.findUsersBySecretId(data.secretId);
if (!organization) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
return;
@@ -1088,10 +1136,14 @@ export const secretQueueFactory = ({
return;
}
const selectedRecipients = recipients?.length
? recipients.map((r) => r.email as string)
: projectMembers.map((m) => m.user.email as string);
await smtpService.sendMail({
template: SmtpTemplates.SecretReminder,
subjectLine: "Infisical secret reminder",
recipients: [...projectMembers.map((m) => m.user.email)].filter((email) => email).map((email) => email as string),
recipients: selectedRecipients,
substitutions: {
reminderNote: data.note, // May not be present.
projectName: project.name,

View File

@@ -546,10 +546,13 @@ export const secretServiceFactory = ({
for await (const secret of secrets) {
if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) {
await secretQueueService.removeSecretReminder({
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
});
await secretQueueService.removeSecretReminder(
{
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
},
tx
);
}
}
@@ -685,6 +688,7 @@ export const secretServiceFactory = ({
...secret,
workspace: projectId,
environment,
secretReminderRecipients: [],
secretPath: groupedPaths[secret.folderId][0].path
}))
};
@@ -1073,10 +1077,13 @@ export const secretServiceFactory = ({
for await (const secret of secrets) {
if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) {
await secretQueueService.removeSecretReminder({
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
});
await secretQueueService.removeSecretReminder(
{
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
},
tx
);
}
}
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
@@ -1786,6 +1793,7 @@ export const secretServiceFactory = ({
tagIds,
secretReminderNote,
secretReminderRepeatDays,
secretReminderRecipients,
metadata,
secretComment,
newSecretName,
@@ -1828,6 +1836,7 @@ export const secretServiceFactory = ({
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays,
secretReminderRecipients,
secretMetadata
}
]
@@ -1837,8 +1846,9 @@ export const secretServiceFactory = ({
}
const secret = await secretV2BridgeService.updateSecret({
secretReminderRepeatDays,
skipMultilineEncoding,
secretReminderNote,
secretReminderRecipients,
skipMultilineEncoding,
tagIds,
secretComment,
secretPath,

View File

@@ -22,9 +22,13 @@ import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote"> & {
secretReminderRecipients?: string[] | null;
};
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id">;
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id"> & {
secretReminderRecipients?: string[] | null;
};
export const FailedIntegrationSyncEmailsPayloadSchema = z.object({
projectId: z.string(),
@@ -258,6 +262,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretReminderRecipients?: string[] | null;
metadata?: {
source?: string;
};
@@ -374,7 +379,7 @@ export type TFnSecretBulkDelete = {
tx?: Knex;
secretDAL: Pick<TSecretDALFactory, "deleteMany">;
secretQueueService: {
removeSecretReminder: (data: TRemoveSecretReminderDTO) => Promise<void>;
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
};
};
@@ -405,11 +410,14 @@ export type TCreateSecretReminderDTO = {
oldSecret: TPartialSecret;
newSecret: TPartialSecret;
projectId: string;
deleteRecipients?: boolean;
};
export type TRemoveSecretReminderDTO = {
secretId: string;
repeatDays: number;
deleteRecipients?: boolean;
};
export type TBackFillSecretReferencesDTO = TProjectPermission;

View File

@@ -12,7 +12,7 @@ require (
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.5.8
github.com/infisical/go-sdk v0.5.92
github.com/infisical/infisical-kmip v0.3.5
github.com/mattn/go-isatty v0.0.20
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a

View File

@@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.5.8 h1:bCetYLp7HWt8DnU9KPh1n8n3z5pjmunkGDB4bA3lEFs=
github.com/infisical/go-sdk v0.5.8/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE=
github.com/infisical/go-sdk v0.5.92/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=

View File

@@ -631,18 +631,18 @@ func sshConnect(cmd *cobra.Command, args []string) {
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
}
writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile")
writeHostCaToFile, err := cmd.Flags().GetBool("write-host-ca-to-file")
if err != nil {
util.HandleError(err, "Unable to parse --writeHostCaToFile flag")
util.HandleError(err, "Unable to parse --write-host-ca-to-file flag")
}
outFilePath, err := cmd.Flags().GetString("outFilePath")
outFilePath, err := cmd.Flags().GetString("out-file-path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
hostname, _ := cmd.Flags().GetString("hostname")
loginUser, _ := cmd.Flags().GetString("loginUser")
loginUser, _ := cmd.Flags().GetString("login-user")
var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string
if outFilePath != "" {
@@ -722,17 +722,24 @@ func sshConnect(cmd *cobra.Command, args []string) {
} else {
hostNames := make([]string, len(hosts))
for i, h := range hosts {
hostNames[i] = h.Hostname
if h.Alias != "" {
hostNames[i] = h.Alias
} else {
hostNames[i] = h.Hostname
}
}
hostPrompt := promptui.Select{
Label: "Select an SSH Host",
Items: hostNames,
Size: 10,
}
hostIdx, _, err := hostPrompt.Run()
if err != nil {
util.HandleError(err, "Prompt failed")
}
selectedHost = hosts[hostIdx]
}
@@ -893,24 +900,33 @@ func sshAddHost(cmd *cobra.Command, args []string) {
util.PrintErrorMessageAndExit("You must provide --hostname")
}
writeUserCaToFile, err := cmd.Flags().GetBool("writeUserCaToFile")
alias, err := cmd.Flags().GetString("alias")
if err != nil {
util.HandleError(err, "Unable to parse --writeUserCaToFile flag")
util.HandleError(err, "Unable to parse --alias flag")
}
// if alias == "" {
// util.PrintErrorMessageAndExit("You must provide --alias")
// }
writeUserCaToFile, err := cmd.Flags().GetBool("write-user-ca-to-file")
if err != nil {
util.HandleError(err, "Unable to parse --write-user-ca-to-file flag")
}
userCaOutFilePath, err := cmd.Flags().GetString("userCaOutFilePath")
userCaOutFilePath, err := cmd.Flags().GetString("user-ca-out-file-path")
if err != nil {
util.HandleError(err, "Unable to parse --userCaOutFilePath flag")
util.HandleError(err, "Unable to parse --user-ca-out-file-path flag")
}
writeHostCertToFile, err := cmd.Flags().GetBool("writeHostCertToFile")
writeHostCertToFile, err := cmd.Flags().GetBool("write-host-cert-to-file")
if err != nil {
util.HandleError(err, "Unable to parse --writeHostCertToFile flag")
util.HandleError(err, "Unable to parse --write-host-cert-to-file flag")
}
configureSshd, err := cmd.Flags().GetBool("configureSshd")
configureSshd, err := cmd.Flags().GetBool("configure-sshd")
if err != nil {
util.HandleError(err, "Unable to parse --configureSshd flag")
util.HandleError(err, "Unable to parse --configure-sshd flag")
}
forceOverwrite, err := cmd.Flags().GetBool("force")
@@ -919,7 +935,7 @@ func sshAddHost(cmd *cobra.Command, args []string) {
}
if configureSshd && (!writeUserCaToFile || !writeHostCertToFile) {
util.PrintErrorMessageAndExit("--configureSshd requires both --writeUserCaToFile and --writeHostCertToFile to also be set")
util.PrintErrorMessageAndExit("--configure-sshd requires both --write-user-ca-to-file and --write-host-cert-to-file to also be set")
}
// Pre-check for file overwrites before proceeding
@@ -927,7 +943,7 @@ func sshAddHost(cmd *cobra.Command, args []string) {
if strings.HasPrefix(userCaOutFilePath, "~") {
homeDir, err := os.UserHomeDir()
if err != nil {
util.HandleError(err, "Unable to resolve ~ in userCaOutFilePath")
util.HandleError(err, "Unable to resolve ~ in user-ca-out-file-path")
}
userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1)
}
@@ -998,6 +1014,7 @@ func sshAddHost(cmd *cobra.Command, args []string) {
host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{
ProjectID: projectId,
Hostname: hostname,
Alias: alias,
})
if err != nil {
util.HandleError(err, "Failed to register SSH host")
@@ -1112,11 +1129,12 @@ func init() {
sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token")
sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)")
sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)")
sshAddHostCmd.Flags().String("alias", "", "Alias for the SSH host")
sshAddHostCmd.Flags().Bool("write-user-ca-to-file", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub")
sshAddHostCmd.Flags().String("user-ca-out-file-path", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key")
sshAddHostCmd.Flags().Bool("write-host-cert-to-file", false, "Write SSH host certificate to /etc/ssh/ssh_host_<type>_key-cert.pub")
sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file")
sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile")
sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update `TrustedUserCAKeys`, `HostKey`, and `HostCertificate` in the `/etc/ssh/sshd_config` file")
sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of `--write-user-ca-to-file` and `--write-host-cert-to-file`")
sshCmd.AddCommand(sshAddHostCmd)

View File

@@ -22,125 +22,72 @@ This command enables you to obtain SSH credentials used to access a remote host.
<Accordion title="--hostname">
The hostname of the SSH host to connect to. If not provided, you will be prompted to select from available hosts.
</Accordion>
<Accordion title="--loginUser">
<Accordion title="--login-user">
The login user for the SSH connection. If not provided, you will be prompted to select from available login users.
</Accordion>
<Accordion title="--writeHostCaToFile">
<Accordion title="--write-host-ca-to-file">
Whether to write the Host CA public key to `~/.ssh/known_hosts` if it doesn't already exist.
Default value: `true`
</Accordion>
<Accordion title="--outFilePath">
<Accordion title="--out-file-path">
The path to write the SSH credentials to such as `~/.ssh`, `./some_folder`, `./some_folder/id_rsa-cert.pub`. If not provided, the credentials will be added to the SSH agent and used to establish an interactive SSH connection.
</Accordion>
<Accordion title="--token">
An authenticated token to use to authenticate with Infisical.
Use a machine identity access token
</Accordion>
</Accordion>
<Accordion title="infisical ssh issue-credentials">
This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template.
We recommend using the `--addToAgent` flag to automatically load issued SSH credentials to the SSH agent.
<Accordion title="infisical ssh add-host">
This command is used to register a new SSH host with Infisical.
This command can be used with the `--write-user-ca-to-file`, `--write-host-cert-to-file`, and `--configure-sshd` flags
to also configure the host's SSH daemon with the necessary certificate authority and host certificate settings.
```bash
$ infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<principals> --addToAgent
$ infisical ssh add-host --projectId=<project-id> --hostname=<hostname>
```
### Flags
<Accordion title="--certificateTemplateId">
The ID of the SSH certificate template to issue SSH credentials for.
<Accordion title="--projectId">
Project ID the host belongs to (required)
</Accordion>
<Accordion title="--principals">
A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for.
<Accordion title="--hostname">
Hostname of the SSH host (required)
</Accordion>
<Accordion title="--addToAgent">
Whether to add issued SSH credentials to the SSH agent.
<Accordion title="--alias">
Alias for the SSH host (optional)
</Accordion>
<Accordion title="--write-user-ca-to-file">
Write User CA public key to `/etc/ssh/infisical_user_ca.pub`
Default value: `false`
</Accordion>
<Accordion title="--user-ca-out-file-path">
Custom file path to write the User CA public key
Default value: `/etc/ssh/infisical_user_ca.pub`
</Accordion>
<Accordion title="--write-host-cert-to-file">
Write SSH host certificate to `/etc/ssh/ssh_host_<type>_key-cert.pub`
Default value: `false`
</Accordion>
<Accordion title="--configure-sshd">
Update `TrustedUserCAKeys`, `HostKey`, and `HostCertificate` in the `/etc/ssh/sshd_config` file
Default value: `false`
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
Note: This flag requires both --write-user-ca-to-file and --write-host-cert-to-file to be set
</Accordion>
<Accordion title="--outFilePath">
The path to write the SSH credentials to such as `~/.ssh`, `./some_folder`, `./some_folder/id_rsa-cert.pub`. If not provided, the credentials will be saved to the current working directory where the command is run.
<Accordion title="--force">
Force overwrite of existing certificate files as part of `--write-user-ca-to-file` and `--write-host-cert-to-file`
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--keyAlgorithm">
The key algorithm to issue SSH credentials for.
Default value: `RSA_2048`
Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`.
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
Defaults to the Default TTL value set in the certificate template.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
Default value: `false`
</Accordion>
<Accordion title="--token">
An authenticated token to use to issue SSH credentials.
</Accordion>
</Accordion>
<Accordion title="infisical ssh sign-key">
This command is used to sign an existing SSH public key against a certificate template; the command outputs the corresponding signed SSH certificate.
```bash
$ infisical ssh sign-key --certificateTemplateId=<certificate-template-id> --publicKey=<public-key> --principals=<principals> --outFilePath=<out-file-path>
```
<Accordion title="--certificateTemplateId">
The ID of the SSH certificate template to issue the SSH certificate for.
</Accordion>
<Accordion title="--publicKey">
The public key to sign.
Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--publicKeyFilePath">
The path to the public key file to sign.
Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--principals">
A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for.
</Accordion>
<Accordion title="--outFilePath">
The path to write the SSH certificate to such as `~/.ssh/id_rsa-cert.pub`; the specified file must have the `.pub` extension. If not provided, the credentials will be saved to the directory of the specified `--publicKeyFilePath` or the current working directory where the command is run.
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
Defaults to the Default TTL value set in the certificate template.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
</Accordion>
<Accordion title="--token">
An authenticated token to use to issue SSH credentials.
Use a machine identity access token
</Accordion>
</Accordion>

View File

@@ -0,0 +1,40 @@
---
title: "Assume Privileges"
description: "Learn how to temporarily assume the privileges of a user or machine identity within a project."
---
This feature allows authorized users to temporarily take on the permissions of another user or identity. It helps administrators and access managers test and verify permissions before granting access, ensuring everything is set up correctly.
It also reduces back-and-forth with end users when troubleshooting permission-related issues.
## How It Works
When an authorized user activates assume privileges mode, they temporarily inherit the target user or identitys permissions for up to one hour.
During this time, they can perform actions within the system with the same level of access as the target user.
- **Permission-based**: Only permissions are inherited, not the full identity
- **Time-limited**: Access automatically expires after one hour
- **Audited**: All actions are logged under the original user's account. This means any action taken during the session will be recorded under the entity assuming the privileges, not the target entity.
- **Authorization required**: Only users with the specific **assume privilege** permission can use this feature
- **Scoped to a single project**: You can only assume privileges for one project at a time
## How to Assume Privileges
<Steps>
<Step title="Go to Project Access">
Click on the user or identity you want to assume.
![Access control page](/images/platform/access-controls/assume-privileges/access-control.png)
</Step>
<Step title="Click Assume Privilege">
Click **Assume Privilege**, then type `assume` to confirm and start your session.
![Access control detail page](/images/platform/access-controls/assume-privileges/access-control-detail.png)
</Step>
<Step title="Session is Active">
You will see a yellow banner indicating that your assume privilege session is active. You can exit at any time by clicking **Exit**.
![session start](/images/platform/access-controls/assume-privileges/session-start.png)
</Step>
</Steps>

View File

@@ -0,0 +1,56 @@
---
title: "GitHub Team Sync"
description: "Learn how to automatically synchronize your GitHub teams with Infisical Groups."
---
## Overview
The GitHub Organization Synchronization feature streamlines user and group management by automatically syncing users belonging to your specified GitHub organization with corresponding groups within Infisical. This integration ensures that users logging in via GitHub are automatically added to or removed from Infisical groups based on their team memberships within your GitHub organization.
## Configuration
To enable and configure GitHub Organization Synchronization, follow these steps:
<Steps>
<Step title="Set up GitHub organization configuration">
1. Navigate to **Organization Settings** and select the **Security Tab**.
![config](../../images/platform/external-syncs/github-org-sync-section.png)
2. Click the **Configure** button and provide the name of your GitHub Organization.
![config-modal](../../images/platform/external-syncs/github-org-sync-config-modal.png)
</Step>
<Step title="Enable GitHub organization sync">
Toggle ON GitHub Organization sync to activate sync.
![toggle-on](../../images/platform/external-syncs/github-org-sync-active.png)
</Step>
<Step title="Approve the Infisical OAuth application on your organization">
Connecting the Infisical OAuth application grants it permission to **read:org** details. This approval is done by selecting your organization during the GitHub OAuth login process.
1. Initiate the login process via the GitHub OAuth flow.
![oauth-flow-start](../../images/platform/external-syncs/github-org-sync-oauth-flow-start.png)
2. Select the organization you have connected.
3. Grant access to Infisical oauth application to your configured organization. Infisical shown here is an organization, just for walkthrough.
![grant-access](../../images/platform/external-syncs/github-org-sync-oauth.png)
<Info>
This action only needs to be done once and authorizes the Infisical OAuth app to read organization details, including team information.
The following users don't need to select organization in GitHub on login anymore.
</Info>
</Step>
</Steps>
## Working
Once configured, the GitHub Organization Synchronization feature functions as follows:
When a user logs in via the GitHub OAuth flow and selects the configured organization, the system will then automatically synchronize the teams they are a part of in GitHub with corresponding groups in Infisical.
## Troubleshooting
<Accordion title="Please check if your organization has approved the Infisical OAuth application.">
If you encounter an error related to this, it indicates that you need to approve the Infisical OAuth application within your GitHub organization.
You can verify the application's approval status by navigating to **https://github.com/organizations/__your-organization__/settings/oauth_application_policy**. Replace `__your-organization__` with the actual name of your GitHub organization.
![check-approval](../../images/platform/external-syncs/github-org-sync-approved-oauth-apps.png)
</Accordion>

View File

@@ -9,6 +9,9 @@ This guide will walk you through the steps needed to configure external KMS supp
## Prerequisites
- An AWS KMS Key configured as a `Symmetric` key and with `Encrypt and Decrypt` key usage.
![Create AWS KMS Key](/images/platform/kms/aws/aws-kms-key-create.png)
Before you begin, you'll first need to choose a method of authentication with AWS from below.
<Tabs>

View File

@@ -268,11 +268,11 @@ For organizations that work with US government agencies, FIPS compliance is almo
<Steps>
<Step title="Create HSM client folder">
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes.
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes. In this example, we are going to be using `/etc/luna-docker`.
```bash
mkdir /etc/hsm-client
mkdir /etc/luna-docker
```
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
@@ -306,20 +306,60 @@ For organizations that work with US government agencies, FIPS compliance is almo
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
```bash
cp -r /<path-to-where-your-hsm-client-is-located> /etc/hsm-client
cp -r /<path-to-where-your-luna-client-is-located>/* /etc/luna-docker
```
<Note>
The `/*` wildcard will copy all files and folders within the HSM client. The wildcard is important to ensure that the file structure is inline with the rest of this guide.
</Note>
After copying the files, the `/etc/luna-docker` directory should have the following file structure:
```bash
$ ls -R /etc/luna-docker
Chrystoki.conf etc lock server-certificate.pem
Chrystoki.conf.tmp2E jsp partition-ca-certificate.pem setenv
lch-support-linux-64bit partition-certificate.pem
bin libs plugins
/etc/luna-docker/bin:
64
/etc/luna-docker/bin/64:
ckdemo cmu lunacm multitoken vtl
/etc/luna-docker/etc:
openssl.cnf
/etc/luna-docker/jsp:
64 LunaProvider.jar
/etc/luna-docker/jsp/64:
libLunaAPI.so
/etc/luna-docker/libs:
64
/etc/luna-docker/libs/64:
libCryptoki2.so
/etc/luna-docker/lock:
/etc/luna-docker/plugins:
libcloud.plugin
```
</Step>
<Step title="Update Chrystoki.conf">
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
In this example, we will be mounting the `/etc/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers.
In this example, we will be mounting the `/etc/luna-docker` folder from the host to containers in our deployment's pods at the path `/usr/safenet/lunaclient`. This means the contents of `/etc/luna-docker` on the host will be accessible at `/usr/safenet/lunaclient` within the containers.
An example config file will look like this:
```Chrystoki.conf
Chrystoki2 = {
# This path points to the mounted path, /hsm-client
LibUNIX64 = /hsm-client/libs/64/libCryptoki2.so;
# This path points to the mounted path, /usr/safenet/lunaclient
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
}
Luna = {
@@ -339,8 +379,8 @@ For organizations that work with US government agencies, FIPS compliance is almo
Misc = {
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
PluginModuleDir = /hsm-client/plugins;
MutexFolder = /hsm-client/lock;
PluginModuleDir = /usr/safenet/lunaclient/plugins;
MutexFolder = /usr/safenet/lunaclient/lock;
PE1746Enabled = 1;
ToolsDir = /usr/bin;
@@ -353,7 +393,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
LunaSA Client = {
ReceiveTimeout = 20000;
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
SSLConfigFile = /hsm-client/etc/openssl.cnf;
SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf;
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
ClientCertFile = ./etc/ClientNameCert.pem;
ServerCAFile = ./etc/CAFile.pem;
@@ -441,7 +481,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
```bash
kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory
kubectl cp ./hsm-client/ hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
kubectl cp /etc/luna-docker/. hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files
```
@@ -456,7 +496,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
We need to update the secret with the following environment variables:
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/usr/safenet/lunaclient/libs/64/libCryptoki2.so`)_
- `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client
- `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
@@ -471,7 +511,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
type: Opaque
stringData:
# ... Other environment variables ...
HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
HSM_LIB_PATH: "/usr/safenet/lunaclient/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
HSM_PIN: "<your-hsm-device-pin>"
HSM_SLOT: "<hsm-device-slot>"
HSM_KEY_LABEL: "<your-key-label>"
@@ -487,7 +527,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
<Step title="Updating the Deployment">
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption.
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with HSM support.
```yaml
# ... The rest of the values.yaml file ...
@@ -499,8 +539,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
extraVolumeMounts:
- name: hsm-data
mountPath: /hsm-client # The path we will mount the HSM client files to
subPath: ./hsm-client
mountPath: /usr/safenet/lunaclient # The path we will mount the HSM client files to
extraVolumes:
- name: hsm-data

View File

@@ -27,6 +27,10 @@ The **Settings** page lets you manage information about your organization includ
![organization settings auth](../../images/platform/organization/organization-settings-auth.png)
<Tip>
You can adjust the maximum time a user token will remain valid for your organization. After this period, users will be required to re-authenticate. This helps improve security by enforcing regular sign-ins.
</Tip>
## Access Control
The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

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