Compare commits

...

191 Commits

Author SHA1 Message Date
sidwebworks
d7517470ed fix: changes 2025-08-07 19:32:58 +05:30
sidwebworks
d9fc4e4972 fix: lint and type check 2025-08-07 18:52:38 +05:30
sidwebworks
4e07d272eb fix: pr changes 2025-08-07 18:47:27 +05:30
Sid
97e28ff9f6 Update backend/src/services/app-connection/render/render-connection-service.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-08-07 18:45:13 +05:30
sidwebworks
28a9c1a1bd fix: update doc 2025-08-07 18:41:23 +05:30
sidwebworks
fd04fa9969 feat: support env groups in render sync 2025-08-07 18:10:48 +05:30
x032205
1ad02e2da6 Merge pull request #4324 from Infisical/mssql-ssl-issue-fix
servername host for mssql
2025-08-06 21:08:21 -04:00
x032205
e105a5f7da servername host for mssql 2025-08-06 19:53:13 -04:00
Scott Wilson
72b80e1fd7 Merge pull request #4323 from Infisical/audit-log-error-message-parsing-fix
fix(frontend): correctly parse fetch audit log error message
2025-08-06 15:47:25 -07:00
Scott Wilson
6429adfaf6 Merge pull request #4322 from Infisical/audit-log-dropdown-overflow
improvement(frontend): update styling and overflow for audit log filter
2025-08-06 15:43:49 -07:00
Scott Wilson
fd89b3c702 fix: correctly parse audit log error message 2025-08-06 15:42:27 -07:00
Scott Wilson
50e40e8bcf improvement: update styling and overflow for audit log filter 2025-08-06 15:17:55 -07:00
x032205
59cffe8cfb Merge pull request #4313 from JuliusMieliauskas/fix-san-extension-contents
FIX: SAN extension field in certificate issuance
2025-08-05 21:26:43 -04:00
Maidul Islam
fa61867a72 Merge pull request #4316 from Infisical/docs/update-self-hostable-ips
Update prerequisites sections for secret syncs/rotations to include being able to accept requests…
2025-08-05 17:45:17 -07:00
Maidul Islam
f3694ca730 add more clarity to notice 2025-08-05 17:44:57 -07:00
Maidul Islam
8fcd6d9997 update phrase and placement 2025-08-05 17:39:02 -07:00
ArshBallagan
45ff9a50b6 update positioning for db related rotations 2025-08-05 15:08:08 -07:00
ArshBallagan
81cdfb9861 update to include secret rotations 2025-08-05 15:06:25 -07:00
ArshBallagan
e1e553ce23 Update prerequisites section to include being bale to accept requests from Infisical 2025-08-05 14:51:09 -07:00
Julius Mieliauskas
e7a6f46f56 refactored SAN validation logic 2025-08-06 00:26:27 +03:00
Daniel Hougaard
b51d997e26 Merge pull request #4270 from Infisical/daniel/srp-removal-round-2
feat: srp removal
2025-08-05 23:47:43 +04:00
Daniel Hougaard
23f6fbe9fc fix: minor (and i mean minor) changes 2025-08-05 23:45:42 +04:00
Sid
c1fb5d8998 docs: add events system pages (#4294)
* feat: events docs

* fix: make the conditions optional in casl check

* Update backend/src/lib/api-docs/constants.ts

* Update backend/src/lib/api-docs/constants.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update docs/docs.json

* docs: content

* fix: pr changes

* feat: improve docs

* chore: remove recursive

* fix: pr changes

* fix: change

* fix: pr changes

* fix: pr changes

* fix: change
2025-08-06 00:43:41 +05:30
Daniel Hougaard
0cb21082c7 requested changes 2025-08-05 22:35:32 +04:00
carlosmonastyrski
4e3613ac6e Merge pull request #4314 from Infisical/fix/editButNotReadValuesFixForCommitRows
Fix edge case where users with edit but not read permission on new commit row logic
2025-08-05 15:32:59 -03:00
carlosmonastyrski
6be65f7a56 Merge pull request #4315 from Infisical/fix/reminderEmptyRecipients
Fix an issue on reminder recipients when all recipients are deleted on an update
2025-08-05 15:32:52 -03:00
Daniel Hougaard
63cb484313 Merge branch 'heads/main' into daniel/srp-removal-round-2 2025-08-05 22:17:01 +04:00
Daniel Hougaard
aa3af1672a requested changes 2025-08-05 22:09:40 +04:00
Daniel Hougaard
33fe11e0fd Update ChangePasswordSection.tsx 2025-08-05 22:05:31 +04:00
Daniel Hougaard
d924a4bccc fix: seeding with a ghost user 2025-08-05 22:05:23 +04:00
Daniel Hougaard
3fc7a71bc7 Update user-service.ts 2025-08-05 22:05:02 +04:00
Daniel Hougaard
986fe2fe23 fix: password resets not working 2025-08-05 22:04:54 +04:00
Carlos Monastyrski
08f7e530b0 Fix edge case where users with edit but not read permission were having a strange behavior on the new commit row logic 2025-08-05 14:40:21 -03:00
Julius Mieliauskas
e9f5055481 fixed SAN extension field in certificate issuance 2025-08-05 20:19:17 +03:00
Scott Wilson
35055955e2 Merge pull request #4298 from Infisical/secret-overview-table-scroll
improvement(frontend): make secret overview table header sticky, add underlines to env header links and limit table height for scroll
2025-08-05 09:04:33 -07:00
carlosmonastyrski
c188e7cd2b Merge pull request #4311 from Infisical/fix/emptyStateIdentityAuthTemplate
Add empty state and improve upgrade plan logic on Identity Auth Templates
2025-08-04 23:19:12 -03:00
carlosmonastyrski
7d2ded6235 Merge pull request #4310 from Infisical/fix/bulkCommitUpdateRowValues
Allow users to type the same original value on bulk commits and remove them if no changes are left
2025-08-04 22:46:25 -03:00
Carlos Monastyrski
aab1a0297e Add empty state and improve upgrade plan logic on Identity Auth Templates 2025-08-04 20:08:26 -03:00
Maidul Islam
dd0f5cebd2 Merge pull request #4301 from Infisical/docs-product-split
Update docs to be multi-product
2025-08-04 14:54:16 -07:00
Maidul Islam
1b29a4564a fix typos 2025-08-04 14:52:47 -07:00
Maidul Islam
9e3c0c8583 fix links 2025-08-04 14:51:02 -07:00
Carlos Monastyrski
3e803debb4 Allow users to type the same original value on bulk commits and remove them if no changes are left 2025-08-04 18:22:30 -03:00
Maidul Islam
16ebe0f8e7 small nits 2025-08-04 14:11:13 -07:00
carlosmonastyrski
e8eb1b5f8b Merge pull request #4300 from Infisical/feat/machineAuthTemplates
Add Machine Auth Templates
2025-08-04 17:24:10 -03:00
x032205
6e37b9f969 Merge pull request #4309 from Infisical/log-available-auth-methods-on-pass-reset
Log available auth methods on password reset
2025-08-04 16:22:44 -04:00
x032205
899b7fe024 Log available auth methods on password reset 2025-08-04 16:16:52 -04:00
Carlos Monastyrski
098a8b81be Final improvements on machine auth templates 2025-08-04 17:01:44 -03:00
Daniel Hougaard
e852cd8b4a Merge pull request #4287 from cyrgim/add-support-image-pull-secret
feat(helm): add support for imagePullSecrets
2025-08-04 23:36:23 +04:00
Carlos Monastyrski
830a2f9581 Renamed identity auth template permissions 2025-08-04 16:28:57 -03:00
Carlos Monastyrski
dc4db40936 Add space between identities tables 2025-08-04 16:14:24 -03:00
Carlos Monastyrski
0beff3cc1c Fixed /ldap-auth/identities/:identityId response schema 2025-08-04 16:05:39 -03:00
x032205
5a3325fc53 Merge pull request #4308 from Infisical/fix-github-hostname-check
fix github hostname check
2025-08-04 14:37:31 -04:00
Carlos Monastyrski
3dde786621 General improvements on auth templates 2025-08-04 15:29:07 -03:00
Akhil Mohan
da6b233db1 Merge pull request #4307 from Infisical/helm-update-v0.9.5
Update Helm chart to version v0.9.5
2025-08-04 23:57:23 +05:30
x032205
6958f1cfbd fix github hostname check 2025-08-04 14:24:09 -04:00
akhilmhdh
adf7a88d67 Update Helm chart to version v0.9.5 2025-08-04 18:22:44 +00:00
Akhil Mohan
b8cd836225 Merge pull request #4296 from Infisical/feat/operator-ldap
feat: ldap auth for k8s operator
2025-08-04 23:46:19 +05:30
=
6826b1c242 feat: made review changed 2025-08-04 23:36:05 +05:30
Daniel Hougaard
35012fde03 fix: added ldap identity auth example 2025-08-04 21:57:07 +04:00
x032205
6e14b2f793 Merge pull request #4306 from Infisical/log-github-error
log github error
2025-08-04 13:48:38 -04:00
x032205
5a3aa3d608 log github error 2025-08-04 13:42:00 -04:00
Daniel Hougaard
95b327de50 Merge pull request #4299 from Infisical/daniel/injector-ldap-auth-docs
docs(agent-injector): ldap auth method
2025-08-04 21:26:27 +04:00
Scott Wilson
a3c36f82f3 Merge pull request #4305 from Infisical/add-react-import-to-email-components
fix: add react import to email button component
2025-08-04 10:22:10 -07:00
Scott Wilson
42612da57d Merge pull request #4293 from Infisical/minor-ui-feedback
improvements: adjust secret search padding when no clear icon and fix access approval reviewer tooltips display
2025-08-04 10:20:32 -07:00
Scott Wilson
f63c07d538 fix: add react import to email button component 2025-08-04 10:12:50 -07:00
x032205
98a08d136e Merge pull request #4302 from Infisical/fix-timeout-for-audit-prune
Add timeout to audit log
2025-08-04 12:28:48 -04:00
x032205
6c74b875f3 up to 10 mins 2025-08-04 10:46:10 -04:00
x032205
793cd4c144 Add timeout to audit log 2025-08-04 10:43:25 -04:00
Tuan Dang
dc0cc4c29d Update images for user + machine identities 2025-08-04 18:48:46 +07:00
Tuan Dang
6dd639be60 Update docs to be multi-product 2025-08-04 16:58:00 +07:00
Carlos Monastyrski
ebe05661d3 Addressed pr comments 2025-08-03 13:02:20 -03:00
Carlos Monastyrski
4f0007faa5 Add Machine Auth Templates 2025-08-03 12:19:57 -03:00
Sid
ec0be1166f feat: Secret reminder from date filter (#4289)
* feat: add fromDate in reminders

* feat: update reminder form

* fix: lint

* chore: generate schema

* fix: reminder logic

* fix: update ui

* fix: pr change

---------

Co-authored-by: sidwebworks <xodeveloper@gmail.com>
2025-08-03 01:10:23 +05:30
Daniel Hougaard
899d01237c docs(agent-injector): ldap auth method 2025-08-02 19:43:27 +04:00
Scott Wilson
ff5dbe74fd Merge pull request #4284 from Infisical/simplify-email-design
improvement(email-templates): simplify email design, refactor link/button to re-usable components and improve design
2025-08-01 18:48:53 -07:00
x032205
24004084f2 Merge pull request #4292 from Infisical/ENG-3422
feat(app-connections): GitHub Enterprise Server support
2025-08-01 21:45:05 -04:00
x032205
0e401ece73 Attempt to use octokit request from dependencies 2025-08-01 21:30:32 -04:00
x032205
c4e1651df7 consistent versioning 2025-08-01 21:19:03 -04:00
x032205
514c7596db Swap away from octokit request 2025-08-01 21:08:15 -04:00
Scott Wilson
9fbdede82c improvements: address feedback 2025-08-01 17:01:51 -07:00
Scott Wilson
1898c16f1b improvement: make secret overview table header sticky, add underlines to env header links and limit table height for scroll 2025-08-01 16:47:11 -07:00
x032205
e519637e89 Fix lint 2025-08-01 18:35:25 -04:00
x032205
ba393b0498 fix dropdown value issue 2025-08-01 18:29:26 -04:00
x032205
4150f81d83 Merge pull request #4282 from JuliusMieliauskas/fix-san-extension-contents
FIX: x509 SAN Extension to accept IPs and URLs as args
2025-08-01 15:24:22 -04:00
Sid
a45bba8537 feat: audit log disable storage flag (#4295)
* feat: audit log disable storage flag

* fix: pr changes

* fix: revert license fns

* Update frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx
2025-08-02 00:29:53 +05:30
x032205
fe7e8e7240 Fix auth baseUrl for octokit 2025-08-01 13:49:38 -04:00
x032205
cf54365022 Update DALs to include gatewayId 2025-08-01 13:47:36 -04:00
=
4b9e57ae61 feat: review changes for reptile 2025-08-01 21:10:26 +05:30
Akhil Mohan
eb27983990 Update k8-operator/packages/util/kubernetes.go
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-08-01 21:08:33 +05:30
=
fa311b032c feat: removed comments 2025-08-01 21:06:17 +05:30
=
71651f85fe docs: ldap auth in operator 2025-08-01 21:04:44 +05:30
=
d28d3449de feat: added ldap authentication to operator 2025-08-01 21:04:29 +05:30
Carlos Monastyrski
14ffa59530 Fix an issue on reminder recipients when all recipients are deleted on an update 2025-08-01 11:47:49 -03:00
Scott Wilson
4f26365c21 improvements: adjust secret search padding when no clear icon and fix access approval reviewer tooltips 2025-07-31 19:58:26 -07:00
x032205
c974df104e Improve types 2025-07-31 20:28:02 -04:00
x032205
e88fdc957e feat(app-connections): GitHub Enterprise Server support 2025-07-31 20:20:24 -04:00
Scott Wilson
55e5360dd4 Merge pull request #4291 from Infisical/server-admin-bulk-delete
improvement(server-admin): add bulk delete users support, bulk actions server admin table support, overflow/truncation and dropdown improvements
2025-07-31 17:19:03 -07:00
Scott Wilson
77a8cd9efc improvement: add bulk delete users support, bulk actions server admin table support, overflow/truncation and dropdown improvements 2025-07-31 16:14:13 -07:00
Julius Mieliauskas
de2c1c5560 removed TLD requirement from SAN extension dns field 2025-07-31 23:51:07 +03:00
Sid
52f773c647 feat: events system implementation (#4246)
* chore: save poc

* chore: save wip

* fix: undo cors

* fix: impl changes

* fix: PR changes

* fix: mocks

* fix: connection tracking and auth changes

* fix: PR changes

* fix: revert license

* feat: frontend change

* fix: revert docker compose.dev

* fix: duplicate publisher connection

* fix: pr changes

* chore: move event impl to `ee`

* fix: lint errors

* fix: check length of events

* fix: static permissions matching

* fix: secretPath

* fix: remove source prefix in bus event name

* fix: license check
2025-08-01 01:20:45 +05:30
Sid
79de7c5f08 feat: Add Netlify app connection and secrets sync (#4205)
* fix: save wip

* feat: final impl

* feat: docs

* Update backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* chore: remove empty conflict files

* Update backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: lint

* feat: Netlify app connection and secrets sync

* feat: docs

* fix: type check

* fix: api client

* fix: lint and types

* fix: typecheck lint

* fix: docs

* fix: lint

* fix: lint

* fix: PR changes

* fix: typecheck

* fix: PR changes

* fix PR changes

* fix: PR Change

* fix: type error

* Small tweaks

* fix: support is_secret

* fix: revert is_secret

* fix: force update existing netlify secret

---------

Co-authored-by: sidwebworks <xodeveloper@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: x032205 <x032205@gmail.com>
2025-08-01 00:24:40 +05:30
x032205
3877fe524d Merge pull request #4286 from Infisical/ENG-3376
feat(app-connections, PKI): Cloudflare as DNS provider
2025-07-31 13:34:31 -04:00
Daniel Hougaard
4c5df70790 Merge pull request #4290 from Infisical/daniel/fix-vault-migration
fix(external-migration/vault): fix vault parsing
2025-07-31 21:28:58 +04:00
x032205
5645dd2b8d Lint + form fixes 2025-07-31 13:21:28 -04:00
Daniel Hougaard
0d55195561 Fixed mailing inconsistency 2025-07-31 21:20:54 +04:00
x032205
1c0caab469 Remove typo 2025-07-31 13:01:04 -04:00
x032205
ed9dfd2974 Docs fix 2025-07-31 12:55:59 -04:00
Daniel Hougaard
7f72037d77 Update vault.ts 2025-07-31 20:54:21 +04:00
x032205
9928ca17ea Greptile review fixes 2025-07-31 12:51:56 -04:00
Julius Mieliauskas
2cbd66e804 changed url validation to use zod 2025-07-31 19:17:08 +03:00
Daniel Hougaard
7357d377e1 Merge pull request #4281 from Infisical/daniel/hsm-support-main-image
chore(hsm): add hsm support to main docker image
2025-07-31 18:03:35 +04:00
cyrgim
4704774c63 feat(helm): add support for imagePullSecrets 2025-07-31 07:01:51 +02:00
x032205
149cecd805 Small tweaks 2025-07-31 00:32:31 -04:00
x032205
c80fd55a74 docs 2025-07-31 00:29:02 -04:00
x032205
93e7723b48 feat(app-connections, PKI): Cloudflare as DNS provider 2025-07-31 00:10:18 -04:00
Scott Wilson
573b990aa3 Merge pull request #4269 from Infisical/org-bulk-user-deletion
improvement(org-memberships): add bulk delete org memberships endpoint and table support
2025-07-30 18:49:57 -07:00
Scott Wilson
e15086edc0 fix: prevent bulk deletion on frontend if scim is enabled 2025-07-30 18:37:58 -07:00
Scott Wilson
4a55ecbe12 improvement: simplify email design, refactor link/button to re-usable components and improve design 2025-07-30 18:14:35 -07:00
Vlad Matsiiako
13ef3809bd Merge pull request #4283 from Infisical/update-favicon
improvement(frontend/docs): update favicon for app and docs
2025-07-30 17:06:38 -07:00
Scott Wilson
fb49c9250a chore: add missing .ico 2025-07-30 17:01:05 -07:00
Scott Wilson
5ced7fa923 improvement: update favicon for app and docs 2025-07-30 16:59:12 -07:00
Scott Wilson
5ffd42378a Merge pull request #4256 from Infisical/gitlab-secret-scanning
feature(secret-scanning): gitlab secret scanning
2025-07-30 16:53:02 -07:00
Julius Mieliauskas
1e29d550be Fix x509 SAN Extension to accept IPs and URLs as args 2025-07-31 02:41:38 +03:00
Scott Wilson
f995708e44 merge main 2025-07-30 16:38:35 -07:00
carlosmonastyrski
c266d68993 Merge pull request #4280 from Infisical/fix/secretApprovalConditionalReadPermissions
Fix conditional permissions check on secret access request hidden values
2025-07-30 20:16:48 -03:00
Daniel Hougaard
c7c8107f85 Update Dockerfile.standalone-infisical 2025-07-31 02:15:08 +04:00
Carlos Monastyrski
b906fe34a1 Fix conditional permissions check on secret access request hidden values 2025-07-30 18:37:54 -03:00
Daniel Hougaard
bec1fefee8 Merge pull request #4271 from Infisical/feat/azureAppConnectionsNewAuth
Add Azure Client Secrets Auth to Azure App Connections
2025-07-30 23:47:15 +04:00
Carlos Monastyrski
cd03107a60 Minor frontend fixes on Azure App Connection forms 2025-07-30 16:42:02 -03:00
Scott Wilson
07965de1db Merge pull request #4279 from Infisical/azure-client-secret-expiry-adjustment
improvement(azure-client-secret-rotation): reduce token expiry to two rotation intervals
2025-07-30 12:01:08 -07:00
Carlos Monastyrski
b20ff0f029 Minor fix on docs titles 2025-07-30 15:35:47 -03:00
Scott Wilson
691cbe0a4f fix: correct issue client secret rotation interval check 2025-07-30 11:15:10 -07:00
x032205
0787128803 Merge pull request #4277 from Infisical/fix-sql-app-conn-gateways
Fix SQL app connection with gateways
2025-07-30 14:09:24 -04:00
Scott Wilson
837158e344 improvement: reduce azure client secret token expiry to two rotation intervals 2025-07-30 11:09:16 -07:00
x032205
03bd1471b2 Revert old "fix" + new bug patch 2025-07-30 13:58:46 -04:00
Scott Wilson
f53c39f65b improvements: address feedback, improve org members table overflow handling, fix user details email/username overflow 2025-07-30 10:43:10 -07:00
Daniel Hougaard
092695089d Merge pull request #4276 from Infisical/daniel/fix-github-app-conn
fix(app-connections): github app connection creation
2025-07-30 21:17:51 +04:00
x032205
2d80681597 Fix 2025-07-30 13:16:48 -04:00
Scott Wilson
cf23f98170 Merge pull request #4259 from Infisical/org-alert-banner-additions
improvement(frontend): revise org alter banner designs and add smtp banner
2025-07-30 10:14:34 -07:00
Daniel Hougaard
c4c8e121f0 Update OauthCallbackPage.tsx 2025-07-30 21:03:36 +04:00
Scott Wilson
0701c996e5 improvement: update smtp link 2025-07-30 09:43:47 -07:00
Scott Wilson
4ca6f165b7 improvement: revise org alter banners and add smtp banner 2025-07-30 09:42:31 -07:00
Scott Wilson
b9dd565926 Merge pull request #4273 from Infisical/improve-initial-app-loading-ui
improvement(frontend): make login/org selection loading screens consistent
2025-07-30 09:11:33 -07:00
Daniel Hougaard
136b0bdcb5 Merge pull request #4275 from Infisical/daniel/update-passport-saml
fix: update passport saml
2025-07-30 18:14:21 +04:00
Daniel Hougaard
7266d1f310 fix: update passport saml 2025-07-30 17:43:57 +04:00
carlosmonastyrski
9c6ec807cb Merge pull request #4212 from Infisical/feat/blockLastPaymentMethodDelete
Prevent users from deleting the last payment method attached to the org
2025-07-30 09:59:50 -03:00
Scott Wilson
756b46428a improvement: make login/org selection loading screens consistent with new loader 2025-07-29 21:34:32 -07:00
Carlos Monastyrski
5fcae35fae Improve azure app connection docs 2025-07-29 22:32:14 -03:00
Carlos Monastyrski
359e19f804 Add Azure Client Secrets Auth to Azure App Connections 2025-07-29 22:05:28 -03:00
Daniel Hougaard
0c98d9187d Update 20250723220500_remove-srp.ts 2025-07-30 05:03:15 +04:00
Daniel Hougaard
e106a6dceb Merge branch 'heads/main' into daniel/srp-removal-round-2 2025-07-30 04:44:57 +04:00
Daniel Hougaard
2d3b1b18d2 feat: srp removal, requested changes 2025-07-30 04:44:25 +04:00
Daniel Hougaard
d5dd2e8bfd feat: srp removal 2025-07-30 04:25:27 +04:00
Scott Wilson
2aa548c7dc improvement: address feedback 2025-07-29 17:06:33 -07:00
Scott Wilson
9d3a382b48 Merge pull request #4258 from Infisical/secret-reference-styling-and-debounce-adjustments
improvement(frontend): improve secret reference styling and reduce debounce
2025-07-29 16:45:37 -07:00
Scott Wilson
4f00fc6777 improvement: add bulk delete org members endpoint and table support 2025-07-29 16:42:13 -07:00
x032205
1f6a63fa71 Merge pull request #4240 from Infisical/ENG-3368
feat(app-connection, secret-sync): Gateway for GitHub App Connections & Secret Syncs
2025-07-29 16:53:36 -04:00
Daniel Hougaard
9e76fa8230 Merge pull request #4267 from Infisical/daniel/fix-fips-x86
fix(fips): x86 support
2025-07-30 00:21:09 +04:00
Daniel Hougaard
e2d4816465 Update release-standalone-docker-img-postgres-offical.yml 2025-07-30 00:16:40 +04:00
carlosmonastyrski
37c8fc80f7 Merge pull request #4265 from Infisical/fix/scimResetsEmailVerification
Fix scim user updates setting isEmailVerified back to false when the email has not changed
2025-07-29 17:06:45 -03:00
Sheen
5ca521ea6b Merge pull request #4266 from Infisical/doc/add-boostrap-to-api-reference
doc: add bootstrap to API reference
2025-07-30 04:02:28 +08:00
Carlos Monastyrski
40de8331a3 Fix scim user updates setting isEmailVerified back to false when the email has not changed 2025-07-29 16:58:06 -03:00
Sheen Capadngan
9374ee3c2e doc: add bootstrap to API reference 2025-07-30 03:57:59 +08:00
Daniel Hougaard
561dbb8835 fix(fips): x86 support 2025-07-29 23:57:46 +04:00
carlosmonastyrski
dece214073 Merge pull request #4264 from Infisical/fix/secretHistoryActorLink
Fix secret version history link to user/machine details page
2025-07-29 14:26:58 -03:00
Carlos Monastyrski
992df5c7d0 Fix secret version history link to user/machine details page 2025-07-29 14:22:39 -03:00
Scott Wilson
00e382d774 Merge pull request #4257 from Infisical/secret-scanning-findings-badge
improvement(frontend): add back secret scanning unresolved finding count to sidebar
2025-07-29 08:14:44 -07:00
Sheen
f63c434c0e Merge pull request #4262 from Infisical/misc/removed-cli
misc: removed CLI repository
2025-07-29 22:21:56 +08:00
Sheen Capadngan
9f0250caf2 misc: removed unnecessary CLI files in root 2025-07-29 20:54:55 +08:00
Sheen Capadngan
d47f6f7ec9 misc: removed CLI directory 2025-07-29 20:49:54 +08:00
Maidul Islam
1126c6b0fa Merge pull request #4244 from Infisical/feature/secrets-detection-in-secrets-manager
feat: secrets detection in secret manager
2025-07-28 23:41:50 -04:00
Maidul Islam
7949142ea7 update text for secret params 2025-07-28 23:32:05 -04:00
Scott Wilson
da28f9224b improvement: improve secret reference styling and reduce debounce for snappier behavior 2025-07-28 16:34:39 -07:00
Scott Wilson
122de99606 improvement: add back secret scanning unresolved finding count to sidebar 2025-07-28 15:29:26 -07:00
Scott Wilson
82b765553c chore: remove unused form variable 2025-07-28 15:22:44 -07:00
Scott Wilson
8972521716 chore: add images 2025-07-28 15:22:19 -07:00
Scott Wilson
81b45b24ec improvement: address greptile feedback 2025-07-28 15:16:10 -07:00
Scott Wilson
f2b0e4ae37 feature: gitlab secret scanning 2025-07-28 15:03:23 -07:00
Sheen Capadngan
57fcfdaf21 Merge remote-tracking branch 'origin/main' into feature/secrets-detection-in-secrets-manager 2025-07-29 04:57:54 +08:00
Sheen Capadngan
e430abfc9e misc: addressed comments 2025-07-29 04:56:50 +08:00
x032205
0b7b32bdc3 Add proper URI component encoding + hostname check 2025-07-25 16:55:21 -04:00
Sheen Capadngan
585cb1b30c misc: used promise all 2025-07-26 03:26:24 +08:00
Sheen Capadngan
7fdee073d8 misc: add secret checker in change policy branch 2025-07-26 03:16:39 +08:00
Sheen Capadngan
c368178cb1 feat: secrets detection in secret manager 2025-07-26 03:00:44 +08:00
x032205
52ef0e6b81 Validate hostname 2025-07-24 22:53:49 -04:00
x032205
0f06c4c27a - Add a max iteration to loop - Hide gateways on frontend if license
does not allow them - Fix capitalization issue with GitHub secret sync
2025-07-24 22:25:23 -04:00
x032205
e34deb7bd0 Frontend tweak 2025-07-24 21:48:43 -04:00
x032205
4b6f9fdec2 docs 2025-07-24 21:47:38 -04:00
x032205
5df7539f65 Swap away from using octokit due to gateway compatibility issues 2025-07-24 21:43:18 -04:00
x032205
2ff211d235 Checkpoint 2025-07-24 16:37:38 -04:00
Carlos Monastyrski
b4ed1fa96a Prevent users from deleting the last payment method attached to the org 2025-07-21 21:17:36 -03:00
861 changed files with 16297 additions and 33175 deletions

View File

@@ -1,153 +0,0 @@
name: Build and release CLI
on:
workflow_dispatch:
push:
# run only against tags
tags:
- "infisical-cli/v*.*.*"
permissions:
contents: write
jobs:
cli-integration-tests:
name: Run tests before deployment
uses: ./.github/workflows/run-cli-tests.yml
secrets:
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
npm-release:
runs-on: ubuntu-latest
env:
working-directory: ./npm
needs:
- cli-integration-tests
- goreleaser
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Extract version
run: |
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
echo "Version extracted: $VERSION"
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
- name: Print version
run: echo ${{ env.CLI_VERSION }}
- name: Setup Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: 20
cache: "npm"
cache-dependency-path: ./npm/package-lock.json
- name: Install dependencies
working-directory: ${{ env.working-directory }}
run: npm install --ignore-scripts
- name: Set NPM version
working-directory: ${{ env.working-directory }}
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
- name: Setup NPM
working-directory: ${{ env.working-directory }}
run: |
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Pack NPM
working-directory: ${{ env.working-directory }}
run: npm pack
- name: Publish NPM
working-directory: ${{ env.working-directory }}
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
goreleaser:
runs-on: ubuntu-latest-8-cores
needs: [cli-integration-tests]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- run: git fetch --force --tags
- run: echo "Ref name ${{github.ref_name}}"
- uses: actions/setup-go@v3
with:
go-version: ">=1.19.3"
cache: true
cache-dependency-path: cli/go.sum
- name: Setup for libssl1.0-dev
run: |
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
sudo apt update
sudo apt-get install -y libssl1.0-dev
- name: OSXCross for CGO Support
run: |
mkdir ../../osxcross
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser-pro
version: v1.26.2-pro
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
- uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
with:
ruby-version: "3.3" # Not needed with a .ruby-version, .tool-versions or mise.toml
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Install deb-s3
run: gem install deb-s3
- name: Configure GPG Key
run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import
env:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
- name: Publish to CloudSmith
run: sh cli/upload_to_cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }}
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
- name: Invalidate Cloudfront cache
run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }}

View File

@@ -1,55 +0,0 @@
name: Go CLI Tests
on:
pull_request:
types: [opened, synchronize]
paths:
- "cli/**"
workflow_dispatch:
workflow_call:
secrets:
CLI_TESTS_UA_CLIENT_ID:
required: true
CLI_TESTS_UA_CLIENT_SECRET:
required: true
CLI_TESTS_SERVICE_TOKEN:
required: true
CLI_TESTS_PROJECT_ID:
required: true
CLI_TESTS_ENV_SLUG:
required: true
CLI_TESTS_USER_EMAIL:
required: true
CLI_TESTS_USER_PASSWORD:
required: true
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE:
required: true
jobs:
test:
defaults:
run:
working-directory: ./cli
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21.x"
- name: Install dependencies
run: go get .
- name: Test with the Go CLI
env:
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
# INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
run: go test -v -count=1 ./test

View File

@@ -1,241 +0,0 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# before:
# hooks:
# # You may remove this if you don't use go modules.
# - cd cli && go mod tidy
# # you may remove this if you don't need go generate
# - cd cli && go generate ./...
before:
hooks:
- ./cli/scripts/completions.sh
- ./cli/scripts/manpages.sh
monorepo:
tag_prefix: infisical-cli/
dir: cli
builds:
- id: darwin-build
binary: infisical
ldflags:
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags:
- -trimpath
env:
- CGO_ENABLED=1
- CC=/home/runner/work/osxcross/target/bin/o64-clang
- CXX=/home/runner/work/osxcross/target/bin/o64-clang++
goos:
- darwin
ignore:
- goos: darwin
goarch: "386"
dir: ./cli
- id: all-other-builds
env:
- CGO_ENABLED=0
binary: infisical
ldflags:
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags:
- -trimpath
goos:
- freebsd
- linux
- netbsd
- openbsd
- windows
goarch:
- "386"
- amd64
- arm
- arm64
goarm:
- "6"
- "7"
ignore:
- goos: windows
goarch: "386"
- goos: freebsd
goarch: "386"
dir: ./cli
archives:
- format_overrides:
- goos: windows
format: zip
files:
- ../README*
- ../LICENSE*
- ../manpages/*
- ../completions/*
release:
replace_existing_draft: true
mode: "replace"
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Version }}-devel"
# publishers:
# - name: fury.io
# ids:
# - infisical
# dir: "{{ dir .ArtifactPath }}"
# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
brews:
- name: infisical
tap:
owner: Infisical
name: homebrew-get-cli
commit_author:
name: "Infisical"
email: ai@infisical.com
folder: Formula
homepage: "https://infisical.com"
description: "The official Infisical CLI"
install: |-
bin.install "infisical"
bash_completion.install "completions/infisical.bash" => "infisical"
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
- name: "infisical@{{.Version}}"
tap:
owner: Infisical
name: homebrew-get-cli
commit_author:
name: "Infisical"
email: ai@infisical.com
folder: Formula
homepage: "https://infisical.com"
description: "The official Infisical CLI"
install: |-
bin.install "infisical"
bash_completion.install "completions/infisical.bash" => "infisical"
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
nfpms:
- id: infisical
package_name: infisical
builds:
- all-other-builds
vendor: Infisical, Inc
homepage: https://infisical.com/
maintainer: Infisical, Inc
description: The offical Infisical CLI
license: MIT
formats:
- rpm
- deb
- apk
- archlinux
bindir: /usr/bin
contents:
- src: ./completions/infisical.bash
dst: /etc/bash_completion.d/infisical
- src: ./completions/infisical.fish
dst: /usr/share/fish/vendor_completions.d/infisical.fish
- src: ./completions/infisical.zsh
dst: /usr/share/zsh/site-functions/_infisical
- src: ./manpages/infisical.1.gz
dst: /usr/share/man/man1/infisical.1.gz
scoop:
bucket:
owner: Infisical
name: scoop-infisical
commit_author:
name: "Infisical"
email: ai@infisical.com
homepage: "https://infisical.com"
description: "The official Infisical CLI"
license: MIT
winget:
- name: infisical
publisher: infisical
license: MIT
homepage: https://infisical.com
short_description: "The official Infisical CLI"
repository:
owner: infisical
name: winget-pkgs
branch: "infisical-{{.Version}}"
pull_request:
enabled: true
draft: false
base:
owner: microsoft
name: winget-pkgs
branch: master
aurs:
- name: infisical-bin
homepage: "https://infisical.com"
description: "The official Infisical CLI"
maintainers:
- Infisical, Inc <support@infisical.com>
license: MIT
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git"
package: |-
# bin
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
# license
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
# completions
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical"
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
# man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
dockers:
- dockerfile: docker/alpine
goos: linux
goarch: amd64
use: buildx
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:latest-amd64"
build_flag_templates:
- "--pull"
- "--platform=linux/amd64"
- dockerfile: docker/alpine
goos: linux
goarch: amd64
use: buildx
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- "infisical/cli:latest-arm64"
build_flag_templates:
- "--pull"
- "--platform=linux/arm64"
docker_manifests:
- name_template: "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- name_template: "infisical/cli:latest"
image_templates:
- "infisical/cli:latest-amd64"
- "infisical/cli:latest-arm64"

View File

@@ -34,6 +34,8 @@ ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
ENV NODE_OPTIONS="--max-old-space-size=8192"
# Build
RUN npm run build
@@ -209,6 +211,11 @@ EXPOSE 443
RUN grep -v 'import "./lib/telemetry/instrumentation.mjs";' dist/main.mjs > dist/main.mjs.tmp && \
mv dist/main.mjs.tmp dist/main.mjs
# The OpenSSL library is installed in different locations in different architectures (x86_64 and arm64).
# This is a workaround to avoid errors when the library is not found.
RUN ln -sf /usr/local/lib64/ossl-modules /usr/local/lib/ossl-modules || \
ln -sf /usr/local/lib/ossl-modules /usr/local/lib64/ossl-modules
USER non-root-user
CMD ["./standalone-entrypoint.sh"]

View File

@@ -55,6 +55,8 @@ USER non-root-user
##
FROM base AS backend-build
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /app
# Install all required dependencies for build
@@ -84,6 +86,8 @@ RUN npm run build
# Production stage
FROM base AS backend-runner
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /app
# Install all required dependencies for runtime
@@ -112,6 +116,11 @@ RUN mkdir frontend-build
FROM base AS production
RUN apt-get update && apt-get install -y \
build-essential \
autoconf \
automake \
libtool \
libssl-dev \
ca-certificates \
bash \
curl \
@@ -171,6 +180,7 @@ ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV NODE_OPTIONS="--max-old-space-size=1024"
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /backend

View File

@@ -7,7 +7,6 @@
"": {
"name": "backend",
"version": "1.0.0",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@aws-sdk/client-elasticache": "^3.637.0",
@@ -34,11 +33,12 @@
"@gitbeaker/rest": "^42.5.0",
"@google-cloud/kms": "^4.5.0",
"@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1",
"@node-saml/passport-saml": "^5.1.0",
"@octokit/auth-app": "^7.1.1",
"@octokit/core": "^5.2.1",
"@octokit/plugin-paginate-graphql": "^4.0.1",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/request": "8.4.1",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1",
@@ -9574,20 +9574,20 @@
}
},
"node_modules/@node-saml/node-saml": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.0.1.tgz",
"integrity": "sha512-YQzFPEC+CnsfO9AFYnwfYZKIzOLx3kITaC1HrjHVLTo6hxcQhc+LgHODOMvW4VCV95Gwrz1MshRUWCPzkDqmnA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.1.0.tgz",
"integrity": "sha512-t3cJnZ4aC7HhPZ6MGylGZULvUtBOZ6FzuUndaHGXjmIZHXnLfC/7L8a57O9Q9V7AxJGKAiRM5zu2wNm9EsvQpw==",
"license": "MIT",
"dependencies": {
"@types/debug": "^4.1.12",
"@types/qs": "^6.9.11",
"@types/qs": "^6.9.18",
"@types/xml-encryption": "^1.2.4",
"@types/xml2js": "^0.4.14",
"@xmldom/is-dom-node": "^1.0.1",
"@xmldom/xmldom": "^0.8.10",
"debug": "^4.3.4",
"xml-crypto": "^6.0.1",
"xml-encryption": "^3.0.2",
"debug": "^4.4.0",
"xml-crypto": "^6.1.2",
"xml-encryption": "^3.1.0",
"xml2js": "^0.6.2",
"xmlbuilder": "^15.1.1",
"xpath": "^0.0.34"
@@ -9597,9 +9597,9 @@
}
},
"node_modules/@node-saml/node-saml/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -9636,14 +9636,14 @@
}
},
"node_modules/@node-saml/passport-saml": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.0.1.tgz",
"integrity": "sha512-fMztg3zfSnjLEgxvpl6HaDMNeh0xeQX4QHiF9e2Lsie2dc4qFE37XYbQZhVmn8XJ2awPpSWLQ736UskYgGU8lQ==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.1.0.tgz",
"integrity": "sha512-pBm+iFjv9eihcgeJuSUs4c0AuX1QEFdHwP8w1iaWCfDzXdeWZxUBU5HT2bY2S4dvNutcy+A9hYsH7ZLBGtgwDg==",
"license": "MIT",
"dependencies": {
"@node-saml/node-saml": "^5.0.1",
"@types/express": "^4.17.21",
"@types/passport": "^1.0.16",
"@node-saml/node-saml": "^5.1.0",
"@types/express": "^4.17.23",
"@types/passport": "^1.0.17",
"@types/passport-strategy": "^0.2.38",
"passport": "^0.7.0",
"passport-strategy": "^1.0.0"
@@ -9778,18 +9778,6 @@
"node": ">= 18"
}
},
"node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -9836,11 +9824,6 @@
"node": "14 || >=16.14"
}
},
"node_modules/@octokit/auth-app/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=="
},
"node_modules/@octokit/auth-oauth-app": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz",
@@ -9856,18 +9839,6 @@
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -9906,11 +9877,6 @@
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/auth-oauth-app/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=="
},
"node_modules/@octokit/auth-oauth-device": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz",
@@ -9925,18 +9891,6 @@
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -9975,11 +9929,6 @@
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/auth-oauth-device/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=="
},
"node_modules/@octokit/auth-oauth-user": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz",
@@ -9995,18 +9944,6 @@
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -10045,11 +9982,6 @@
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/auth-oauth-user/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=="
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
@@ -10103,32 +10035,38 @@
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/core/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"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==",
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"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==",
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"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==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/graphql": {
@@ -10160,6 +10098,12 @@
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/graphql/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"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",
@@ -10182,18 +10126,6 @@
"node": ">= 18"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -10232,11 +10164,6 @@
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/oauth-methods/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=="
},
"node_modules/@octokit/openapi-types": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
@@ -10377,31 +10304,54 @@
}
},
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
"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/request-error/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"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": "^22.2.0"
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/request/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/request/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
"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/request/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"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": "^22.2.0"
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/request/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/@octokit/rest": {
"version": "20.0.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
@@ -13351,9 +13301,10 @@
"license": "MIT"
},
"node_modules/@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -13523,9 +13474,10 @@
}
},
"node_modules/@types/passport": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
"integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==",
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
@@ -18287,7 +18239,8 @@
"node_modules/fast-content-type-parse": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ=="
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==",
"license": "MIT"
},
"node_modules/fast-copy": {
"version": "3.0.1",
@@ -24775,6 +24728,12 @@
"jsonwebtoken": "^9.0.2"
}
},
"node_modules/octokit-auth-probot/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/odbc": {
"version": "2.4.9",
"resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz",
@@ -30704,9 +30663,10 @@
"integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ=="
},
"node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
},
"node_modules/universalify": {
"version": "2.0.1",
@@ -31953,9 +31913,9 @@
"license": "MIT"
},
"node_modules/xml-crypto": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz",
"integrity": "sha512-v05aU7NS03z4jlZ0iZGRFeZsuKO1UfEbbYiaeRMiATBFs6Jq9+wqKquEMTn4UTrYZ9iGD8yz3KT4L9o2iF682w==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.1.2.tgz",
"integrity": "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==",
"license": "MIT",
"dependencies": {
"@xmldom/is-dom-node": "^1.0.1",

View File

@@ -153,11 +153,12 @@
"@gitbeaker/rest": "^42.5.0",
"@google-cloud/kms": "^4.5.0",
"@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1",
"@node-saml/passport-saml": "^5.1.0",
"@octokit/auth-app": "^7.1.1",
"@octokit/core": "^5.2.1",
"@octokit/plugin-paginate-graphql": "^4.0.1",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/request": "8.4.1",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1",

View File

@@ -99,6 +99,7 @@ const main = async () => {
(el) =>
!el.tableName.includes("_migrations") &&
!el.tableName.includes("audit_logs_") &&
!el.tableName.includes("active_locks") &&
el.tableName !== "intermediate_audit_logs"
);

View File

@@ -12,10 +12,13 @@ import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certifi
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-types";
import { TEventBusService } from "@app/ee/services/event/event-bus-service";
import { TServerSentEventsService } from "@app/ee/services/event/event-sse-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 { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template";
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";
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
@@ -296,6 +299,9 @@ declare module "fastify" {
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
pkiTemplate: TPkiTemplatesServiceFactory;
reminder: TReminderServiceFactory;
bus: TEventBusService;
sse: TServerSentEventsService;
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@@ -494,6 +494,11 @@ import {
TAccessApprovalPoliciesEnvironmentsInsert,
TAccessApprovalPoliciesEnvironmentsUpdate
} from "@app/db/schemas/access-approval-policies-environments";
import {
TIdentityAuthTemplates,
TIdentityAuthTemplatesInsert,
TIdentityAuthTemplatesUpdate
} from "@app/db/schemas/identity-auth-templates";
import {
TIdentityLdapAuths,
TIdentityLdapAuthsInsert,
@@ -878,6 +883,11 @@ declare module "knex/types/tables" {
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate
>;
[TableName.IdentityAuthTemplate]: KnexOriginal.CompositeTableType<
TIdentityAuthTemplates,
TIdentityAuthTemplatesInsert,
TIdentityAuthTemplatesUpdate
>;
[TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType<
TAccessApprovalPolicies,

View File

@@ -0,0 +1,18 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => {
table.text("encryptedPrivateKey").nullable().alter();
table.text("publicKey").nullable().alter();
table.text("iv").nullable().alter();
table.text("tag").nullable().alter();
table.text("salt").nullable().alter();
table.text("verifier").nullable().alter();
});
}
export async function down(): Promise<void> {
// do nothing for now to avoid breaking down migrations
}

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues"))) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.specificType("secretDetectionIgnoreValues", "text[]");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues")) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("secretDetectionIgnoreValues");
});
}
}

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.Reminder, "fromDate"))) {
await knex.schema.alterTable(TableName.Reminder, (t) => {
t.timestamp("fromDate", { useTz: true }).nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Reminder, "fromDate")) {
await knex.schema.alterTable(TableName.Reminder, (t) => {
t.dropColumn("fromDate");
});
}
}

View File

@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityAuthTemplate))) {
await knex.schema.createTable(TableName.IdentityAuthTemplate, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.binary("templateFields").notNullable();
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("name", 64).notNullable();
t.string("authMethod").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
}
if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId"))) {
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
t.uuid("templateId").nullable();
t.foreign("templateId").references("id").inTable(TableName.IdentityAuthTemplate).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId")) {
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
t.dropForeign(["templateId"]);
t.dropColumn("templateId");
});
}
await knex.schema.dropTableIfExists(TableName.IdentityAuthTemplate);
await dropOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
}

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 IdentityAuthTemplatesSchema = z.object({
id: z.string().uuid(),
templateFields: zodBuffer,
orgId: z.string().uuid(),
name: z.string(),
authMethod: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityAuthTemplates = z.infer<typeof IdentityAuthTemplatesSchema>;
export type TIdentityAuthTemplatesInsert = Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>;
export type TIdentityAuthTemplatesUpdate = Partial<Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>>;

View File

@@ -25,7 +25,8 @@ export const IdentityLdapAuthsSchema = z.object({
allowedFields: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
accessTokenPeriod: z.coerce.number().default(0)
accessTokenPeriod: z.coerce.number().default(0),
templateId: z.string().uuid().nullable().optional()
});
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;

View File

@@ -91,6 +91,7 @@ export enum TableName {
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
IdentityAuthTemplate = "identity_auth_templates",
// used by both identity and users
IdentityMetadata = "identity_metadata",
ResourceMetadata = "resource_metadata",

View File

@@ -30,7 +30,8 @@ export const ProjectsSchema = z.object({
hasDeleteProtection: z.boolean().default(false).nullable().optional(),
secretSharing: z.boolean().default(true),
showSnapshotsLegacy: z.boolean().default(false),
defaultProduct: z.string().nullable().optional()
defaultProduct: z.string().nullable().optional(),
secretDetectionIgnoreValues: z.string().array().nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -14,7 +14,8 @@ export const RemindersSchema = z.object({
repeatDays: z.number().nullable().optional(),
nextReminderDate: z.date(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
fromDate: z.date().nullable().optional()
});
export type TReminders = z.infer<typeof RemindersSchema>;

View File

@@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
protectedKey: z.string().nullable().optional(),
protectedKeyIV: z.string().nullable().optional(),
protectedKeyTag: z.string().nullable().optional(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
salt: z.string(),
verifier: z.string(),
publicKey: z.string().nullable().optional(),
encryptedPrivateKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
salt: z.string().nullable().optional(),
verifier: z.string().nullable().optional(),
userId: z.string().uuid(),
hashedPassword: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(),

View File

@@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
};
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new Error("User encrypted private key not found");
}
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,

View File

@@ -1,7 +1,7 @@
import { Knex } from "knex";
import { crypto } from "@app/lib/crypto";
import { initLogger } from "@app/lib/logger";
import { initEnvConfig } from "@app/lib/config/env";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { AuthMethod } from "../../services/auth/auth-type";
@@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await crypto.initialize(superAdminDAL);
await initEnvConfig(superAdminDAL, logger);
await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line
@@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise<void> {
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]);
// Inserts seed entries
const [user] = await knex(TableName.Users)
.insert([
{

View File

@@ -1,9 +1,28 @@
import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { initLogger, logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AuthMethod } from "@app/services/auth/auth-type";
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { userDALFactory } from "@app/services/user/user-dal";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
import {
OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
TableName
} from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
@@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [
{ name: "Production", slug: "prod" }
];
const createUserWithGhostUser = async (
orgId: string,
projectId: string,
userId: string,
userOrgMembershipId: string,
knex: Knex
) => {
const projectKeyDAL = projectKeyDALFactory(knex);
const userDAL = userDALFactory(knex);
const projectMembershipDAL = projectMembershipDALFactory(knex);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex);
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
const password = crypto.randomBytes(128).toString("hex");
const [ghostUser] = await knex(TableName.Users)
.insert({
isGhost: true,
authMethods: [AuthMethod.EMAIL],
username: email,
email,
isAccepted: true
})
.returning("*");
const encKeys = await generateUserSrpKeys(email, password);
await knex(TableName.UserEncryptionKey)
.insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey })
.onConflict("userId")
.merge();
await knex(TableName.OrgMembership)
.insert({
orgId,
userId: ghostUser.id,
role: OrgMembershipRole.Admin,
status: OrgMembershipStatus.Accepted,
isActive: true
})
.returning("*");
const [projectMembership] = await knex(TableName.ProjectMembership)
.insert({
userId: ghostUser.id,
projectId
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
projectMembershipId: projectMembership.id,
role: ProjectMembershipRole.Admin
});
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: encKeys.publicKey,
privateKey: encKeys.plainPrivateKey
});
await knex(TableName.ProjectKeys).insert({
projectId,
receiverId: ghostUser.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.id
});
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(encKeys.plainPrivateKey);
await knex(TableName.ProjectBot).insert({
name: "Infisical Bot (Ghost)",
projectId,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: encKeys.publicKey,
senderId: ghostUser.id,
algorithm,
keyEncoding: encoding
});
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
const user = await userDAL.findUserEncKeyByUserId(userId, knex);
if (!user || !user.publicKey) {
throw new Error("User not found");
}
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: encKeys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: userOrgMembershipId
}
]
});
// Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create(
{
projectId,
userId: user.id
},
knex
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
knex
);
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
},
knex
);
return {
user: ghostUser,
keys: encKeys
};
};
export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries
await knex(TableName.Project).del();
await knex(TableName.Environment).del();
await knex(TableName.SecretFolder).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
const [project] = await knex(TableName.Project)
.insert({
name: seedData1.project.name,
@@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise<void> {
})
.returning("*");
const projectMembership = await knex(TableName.ProjectMembership)
.insert({
projectId: project.id,
const userOrgMembership = await knex(TableName.OrgMembership)
.where({
orgId: seedData1.organization.id,
userId: seedData1.id
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembership[0].id
});
.first();
if (!userOrgMembership) {
throw new Error("User org membership not found");
}
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
if (!user) throw new Error("User not found");
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
await knex(TableName.ProjectKeys).insert({
projectId: project.id,
nonce: projectKey.nonce,
encryptedKey: projectKey.ciphertext,
receiverId: seedData1.id,
senderId: seedData1.id
});
if (!user.publicKey) {
throw new Error("User public key not found");
}
await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex);
// create default environments and default folders
const envs = await knex(TableName.Environment)

View File

@@ -1,6 +1,9 @@
import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
@@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
// Inserts seed entries
await knex(TableName.Identity).insert([
{

View File

@@ -0,0 +1,391 @@
import { z } from "zod";
import { IdentityAuthTemplatesSchema } from "@app/db/schemas/identity-auth-templates";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
IdentityAuthTemplateMethod,
TEMPLATE_SUCCESS_MESSAGES,
TEMPLATE_VALIDATION_MESSAGES
} from "@app/ee/services/identity-auth-template/identity-auth-template-enums";
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 ldapTemplateFieldsSchema = z.object({
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED),
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED),
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED),
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED),
ldapCaCertificate: z.string().trim().optional()
});
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Create identity auth template",
security: [
{
bearerAuth: []
}
],
body: z.object({
name: z
.string()
.trim()
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH),
authMethod: z.nativeEnum(IdentityAuthTemplateMethod),
templateFields: ldapTemplateFieldsSchema
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: z.record(z.string(), z.unknown())
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.createTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.body.name,
authMethod: req.body.authMethod,
templateFields: req.body.templateFields
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return template;
}
});
server.route({
method: "PATCH",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Update identity auth template",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
body: z.object({
name: z
.string()
.trim()
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH)
.optional(),
templateFields: ldapTemplateFieldsSchema.partial().optional()
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: z.record(z.string(), z.unknown())
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.updateTemplate({
templateId: req.params.templateId,
name: req.body.name,
templateFields: req.body.templateFields,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return template;
}
});
server.route({
method: "DELETE",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Delete identity auth template",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.deleteTemplate({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return { message: TEMPLATE_SUCCESS_MESSAGES.DELETED };
}
});
server.route({
method: "GET",
url: "/:templateId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get identity auth template by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.getTemplate({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return template;
}
});
server.route({
method: "GET",
url: "/search",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "List identity auth templates",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
limit: z.coerce.number().positive().max(100).default(5).optional(),
offset: z.coerce.number().min(0).default(0).optional(),
search: z.string().optional()
}),
response: {
200: z.object({
templates: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
}).array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { templates, totalCount } = await server.services.identityAuthTemplate.listTemplates({
limit: req.query.limit,
offset: req.query.offset,
search: req.query.search,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { templates, totalCount };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get identity auth templates by authentication method",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
authMethod: z.nativeEnum(IdentityAuthTemplateMethod)
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
}).array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.getTemplatesByAuthMethod({
authMethod: req.query.authMethod,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
server.route({
method: "GET",
url: "/:templateId/usage",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get template usage by template ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string()
}),
response: {
200: z
.object({
identityId: z.string(),
identityName: z.string()
})
.array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.findTemplateUsages({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
server.route({
method: "POST",
url: "/:templateId/delete-usage",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Unlink identity auth template usage",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string()
}),
body: z.object({
identityIds: z.string().array()
}),
response: {
200: z
.object({
authId: z.string(),
identityId: z.string(),
identityName: z.string()
})
.array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.unlinkTemplateUsage({
templateId: req.params.templateId,
identityIds: req.body.identityIds,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
};

View File

@@ -13,6 +13,7 @@ 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 { registerIdentityTemplateRouter } from "./identity-template-router";
import { registerKmipRouter } from "./kmip-router";
import { registerKmipSpecRouter } from "./kmip-spec-router";
import { registerLdapRouter } from "./ldap-router";
@@ -125,6 +126,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });

View File

@@ -0,0 +1,16 @@
import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints";
import {
CreateGitLabDataSourceSchema,
GitLabDataSourceSchema,
UpdateGitLabDataSourceSchema
} from "@app/ee/services/secret-scanning-v2/gitlab";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
export const registerGitLabSecretScanningRouter = async (server: FastifyZodProvider) =>
registerSecretScanningEndpoints({
type: SecretScanningDataSource.GitLab,
server,
responseSchema: GitLabDataSourceSchema,
createSchema: CreateGitLabDataSourceSchema,
updateSchema: UpdateGitLabDataSourceSchema
});

View File

@@ -1,3 +1,4 @@
import { registerGitLabSecretScanningRouter } from "@app/ee/routes/v2/secret-scanning-v2-routers/gitlab-secret-scanning-router";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router";
@@ -10,5 +11,6 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
(server: FastifyZodProvider) => Promise<void>
> = {
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter,
[SecretScanningDataSource.GitLab]: registerGitLabSecretScanningRouter
};

View File

@@ -4,6 +4,7 @@ import { SecretScanningConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { BitbucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
import { GitLabDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
import {
SecretScanningFindingStatus,
SecretScanningScanStatus
@@ -24,7 +25,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
GitHubDataSourceListItemSchema,
BitbucketDataSourceListItemSchema
BitbucketDataSourceListItemSchema,
GitLabDataSourceListItemSchema
]);
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {

View File

@@ -1,8 +1,10 @@
// weird commonjs-related error in the CI requires us to do the import like this
import knex from "knex";
import { v4 as uuidv4 } from "uuid";
import { TDbClient } from "@app/db";
import { TableName, TAuditLogs } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
@@ -150,43 +152,70 @@ export const auditLogDALFactory = (db: TDbClient) => {
// delete all audit log that have expired
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
const runPrune = async (dbClient: knex.Knex) => {
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
const today = new Date();
let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
const today = new Date();
let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
do {
try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
.where("expiresAt", "<", today)
.where("createdAt", "<", today) // to use audit log partition
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
.select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
do {
try {
const findExpiredLogSubQuery = dbClient(TableName.AuditLog)
.where("expiresAt", "<", today)
.where("createdAt", "<", today) // to use audit log partition
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
.select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
.whereIn("id", findExpiredLogSubQuery)
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
// eslint-disable-next-line no-await-in-loop
deletedAuditLogIds = await dbClient(TableName.AuditLog)
.whereIn("id", findExpiredLogSubQuery)
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
};
if (tx) {
await runPrune(tx);
} else {
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
await db.transaction(async (trx) => {
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
await runPrune(trx);
});
}
};
return { ...auditLogOrm, pruneAuditLog, find };
const create: TAuditLogDALFactory["create"] = async (tx) => {
const config = getConfig();
if (config.DISABLE_AUDIT_LOG_STORAGE) {
return {
...tx,
id: uuidv4(),
createdAt: new Date(),
updatedAt: new Date()
};
}
return auditLogOrm.create(tx);
};
return { ...auditLogOrm, create, pruneAuditLog, find };
};

View File

@@ -1,7 +1,8 @@
import { AxiosError, RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
import { TEventBusService } from "@app/ee/services/event/event-bus-service";
import { TopicName, toPublishableEvent } from "@app/ee/services/event/types";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto/cryptography";
import { logger } from "@app/lib/logger";
@@ -21,6 +22,7 @@ type TAuditLogQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
projectDAL: Pick<TProjectDALFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
eventBusService: TEventBusService;
};
export type TAuditLogQueueServiceFactory = {
@@ -36,133 +38,17 @@ export const auditLogQueueServiceFactory = async ({
queueService,
projectDAL,
licenseService,
auditLogStreamDAL
auditLogStreamDAL,
eventBusService
}: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
const appCfg = getConfig();
const pushToLog = async (data: TCreateAuditLogDTO) => {
if (appCfg.USE_PG_QUEUE && appCfg.SHOULD_INIT_PG_QUEUE) {
await queueService.queuePg<QueueName.AuditLog>(QueueJobs.AuditLog, data, {
retryLimit: 10,
retryBackoff: true
});
} else {
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
removeOnFail: {
count: 3
},
removeOnComplete: true
});
}
};
if (appCfg.SHOULD_INIT_PG_QUEUE) {
await queueService.startPg<QueueName.AuditLog>(
QueueJobs.AuditLog,
async ([job]) => {
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
let { orgId } = job.data;
const MS_IN_DAY = 24 * 60 * 60 * 1000;
let project;
if (!orgId) {
// it will never be undefined for both org and project id
// TODO(akhilmhdh): use caching here in dal to avoid db calls
project = await projectDAL.findById(projectId as string);
orgId = project.orgId;
}
const plan = await licenseService.getPlan(orgId);
if (plan.auditLogsRetentionDays === 0) {
// skip inserting if audit log retention is 0 meaning its not supported
return;
}
// For project actions, set TTL to project-level audit log retention config
// This condition ensures that the plan's audit log retention days cannot be bypassed
const ttlInDays =
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
? project.auditLogsRetentionDays
: plan.auditLogsRetentionDays;
const ttl = ttlInDays * MS_IN_DAY;
const auditLog = await auditLogDAL.create({
actor: actor.type,
actorMetadata: actor.metadata,
userAgent,
projectId,
projectName: project?.name,
ipAddress,
orgId,
eventType: event.type,
expiresAt: new Date(Date.now() + ttl),
eventMetadata: event.metadata,
userAgentType
});
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
await Promise.allSettled(
logStreams.map(
async ({
url,
encryptedHeadersTag,
encryptedHeadersIV,
encryptedHeadersKeyEncoding,
encryptedHeadersCiphertext
}) => {
const streamHeaders =
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
? (JSON.parse(
crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
iv: encryptedHeadersIV,
tag: encryptedHeadersTag,
ciphertext: encryptedHeadersCiphertext
})
) as LogStreamHeaders[])
: [];
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (streamHeaders.length)
streamHeaders.forEach(({ key, value }) => {
headers[key] = value;
});
try {
const response = await request.post(
url,
{ ...providerSpecificPayload(url), ...auditLog },
{
headers,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
}
);
return response;
} catch (error) {
logger.error(
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
);
return error;
}
}
)
);
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
removeOnFail: {
count: 3
},
{
batchSize: 1,
workerCount: 30,
pollingIntervalSeconds: 0.5
}
);
}
removeOnComplete: true
});
};
queueService.start(QueueName.AuditLog, async (job) => {
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
@@ -178,88 +64,97 @@ export const auditLogQueueServiceFactory = async ({
}
const plan = await licenseService.getPlan(orgId);
if (plan.auditLogsRetentionDays === 0) {
// skip inserting if audit log retention is 0 meaning its not supported
return;
// skip inserting if audit log retention is 0 meaning its not supported
if (plan.auditLogsRetentionDays !== 0) {
// For project actions, set TTL to project-level audit log retention config
// This condition ensures that the plan's audit log retention days cannot be bypassed
const ttlInDays =
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
? project.auditLogsRetentionDays
: plan.auditLogsRetentionDays;
const ttl = ttlInDays * MS_IN_DAY;
const auditLog = await auditLogDAL.create({
actor: actor.type,
actorMetadata: actor.metadata,
userAgent,
projectId,
projectName: project?.name,
ipAddress,
orgId,
eventType: event.type,
expiresAt: new Date(Date.now() + ttl),
eventMetadata: event.metadata,
userAgentType
});
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
await Promise.allSettled(
logStreams.map(
async ({
url,
encryptedHeadersTag,
encryptedHeadersIV,
encryptedHeadersKeyEncoding,
encryptedHeadersCiphertext
}) => {
const streamHeaders =
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
? (JSON.parse(
crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
iv: encryptedHeadersIV,
tag: encryptedHeadersTag,
ciphertext: encryptedHeadersCiphertext
})
) as LogStreamHeaders[])
: [];
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (streamHeaders.length)
streamHeaders.forEach(({ key, value }) => {
headers[key] = value;
});
try {
const response = await request.post(
url,
{ ...providerSpecificPayload(url), ...auditLog },
{
headers,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
}
);
return response;
} catch (error) {
logger.error(
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
);
return error;
}
}
)
);
}
// For project actions, set TTL to project-level audit log retention config
// This condition ensures that the plan's audit log retention days cannot be bypassed
const ttlInDays =
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
? project.auditLogsRetentionDays
: plan.auditLogsRetentionDays;
const publishable = toPublishableEvent(event);
const ttl = ttlInDays * MS_IN_DAY;
const auditLog = await auditLogDAL.create({
actor: actor.type,
actorMetadata: actor.metadata,
userAgent,
projectId,
projectName: project?.name,
ipAddress,
orgId,
eventType: event.type,
expiresAt: new Date(Date.now() + ttl),
eventMetadata: event.metadata,
userAgentType
});
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
await Promise.allSettled(
logStreams.map(
async ({
url,
encryptedHeadersTag,
encryptedHeadersIV,
encryptedHeadersKeyEncoding,
encryptedHeadersCiphertext
}) => {
const streamHeaders =
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
? (JSON.parse(
crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
iv: encryptedHeadersIV,
tag: encryptedHeadersTag,
ciphertext: encryptedHeadersCiphertext
})
) as LogStreamHeaders[])
: [];
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (streamHeaders.length)
streamHeaders.forEach(({ key, value }) => {
headers[key] = value;
});
try {
const response = await request.post(
url,
{ ...providerSpecificPayload(url), ...auditLog },
{
headers,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
}
);
return response;
} catch (error) {
logger.error(
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
);
return error;
}
}
)
);
if (publishable) {
await eventBusService.publish(TopicName.CoreServers, {
type: ProjectType.SecretManager,
source: "infiscal",
data: publishable.data
});
}
});
return {

View File

@@ -161,6 +161,9 @@ export enum EventType {
CREATE_IDENTITY = "create-identity",
UPDATE_IDENTITY = "update-identity",
DELETE_IDENTITY = "delete-identity",
MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create",
MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update",
MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete",
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
@@ -830,6 +833,30 @@ interface LoginIdentityUniversalAuthEvent {
};
}
interface MachineIdentityAuthTemplateCreateEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE;
metadata: {
templateId: string;
name: string;
};
}
interface MachineIdentityAuthTemplateUpdateEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE;
metadata: {
templateId: string;
name: string;
};
}
interface MachineIdentityAuthTemplateDeleteEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE;
metadata: {
templateId: string;
name: string;
};
}
interface AddIdentityUniversalAuthEvent {
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
metadata: {
@@ -1325,6 +1352,7 @@ interface AddIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[];
url: string;
templateId?: string | null;
};
}
@@ -1338,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[];
url?: string;
templateId?: string | null;
};
}
@@ -3439,6 +3468,9 @@ export type Event =
| UpdateIdentityEvent
| DeleteIdentityEvent
| LoginIdentityUniversalAuthEvent
| MachineIdentityAuthTemplateCreateEvent
| MachineIdentityAuthTemplateUpdateEvent
| MachineIdentityAuthTemplateDeleteEvent
| AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent
| DeleteIdentityUniversalAuthEvent

View File

@@ -0,0 +1,83 @@
import Redis from "ioredis";
import { z } from "zod";
import { logger } from "@app/lib/logger";
import { EventSchema, TopicName } from "./types";
export const eventBusFactory = (redis: Redis) => {
const publisher = redis.duplicate();
// Duplicate the publisher to create a subscriber.
// This is necessary because Redis does not allow a single connection to both publish and subscribe.
const subscriber = publisher.duplicate();
const init = async (topics: TopicName[] = Object.values(TopicName)) => {
subscriber.on("error", (e) => {
logger.error(e, "Event Bus subscriber error");
});
publisher.on("error", (e) => {
logger.error(e, "Event Bus publisher error");
});
await subscriber.subscribe(...topics);
};
/**
* Publishes an event to the specified topic.
* @param topic - The topic to publish the event to.
* @param event - The event data to publish.
*/
const publish = async <T extends z.input<typeof EventSchema>>(topic: TopicName, event: T) => {
const json = JSON.stringify(event);
return publisher.publish(topic, json, (err) => {
if (err) {
return logger.error(err, `Error publishing to channel ${topic}`);
}
});
};
/**
* @param fn - The function to call when a message is received.
* It should accept the parsed event data as an argument.
* @template T - The type of the event data, which should match the schema defined in EventSchema.
* @returns A function that can be called to unsubscribe from the event bus.
*/
const subscribe = <T extends z.infer<typeof EventSchema>>(fn: (data: T) => Promise<void> | void) => {
// Not using async await cause redis client's `on` method does not expect async listeners.
const listener = (channel: string, message: string) => {
try {
const parsed = JSON.parse(message) as T;
const thenable = fn(parsed);
// If the function returns a Promise, catch any errors that occur during processing.
if (thenable instanceof Promise) {
thenable.catch((error) => {
logger.error(error, `Error processing message from channel ${channel}`);
});
}
} catch (error) {
logger.error(error, `Error parsing message data from channel ${channel}`);
}
};
subscriber.on("message", listener);
return () => {
subscriber.off("message", listener);
};
};
const close = async () => {
try {
await publisher.quit();
await subscriber.quit();
} catch (error) {
logger.error(error, "Error closing event bus connections");
}
};
return { init, publish, subscribe, close };
};
export type TEventBusService = ReturnType<typeof eventBusFactory>;

View File

@@ -0,0 +1,162 @@
/* eslint-disable no-continue */
import { subject } from "@casl/ability";
import Redis from "ioredis";
import { KeyStorePrefixes } from "@app/keystore/keystore";
import { logger } from "@app/lib/logger";
import { TEventBusService } from "./event-bus-service";
import { createEventStreamClient, EventStreamClient, IEventStreamClientOpts } from "./event-sse-stream";
import { EventData, RegisteredEvent, toBusEventName } from "./types";
const AUTH_REFRESH_INTERVAL = 60 * 1000;
const HEART_BEAT_INTERVAL = 15 * 1000;
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
const clients = new Set<EventStreamClient>();
const heartbeatInterval = setInterval(() => {
for (const client of clients) {
if (client.stream.closed) continue;
void client.ping();
}
}, HEART_BEAT_INTERVAL);
const refreshInterval = setInterval(() => {
for (const client of clients) {
if (client.stream.closed) continue;
void client.refresh();
}
}, AUTH_REFRESH_INTERVAL);
const removeActiveConnection = async (projectId: string, identityId: string, connectionId: string) => {
const set = KeyStorePrefixes.ActiveSSEConnectionsSet(projectId, identityId);
const key = KeyStorePrefixes.ActiveSSEConnections(projectId, identityId, connectionId);
await Promise.all([redis.lrem(set, 0, connectionId), redis.del(key)]);
};
const getActiveConnectionsCount = async (projectId: string, identityId: string) => {
const set = KeyStorePrefixes.ActiveSSEConnectionsSet(projectId, identityId);
const connections = await redis.lrange(set, 0, -1);
if (connections.length === 0) {
return 0; // No active connections
}
const keys = connections.map((c) => KeyStorePrefixes.ActiveSSEConnections(projectId, identityId, c));
const values = await redis.mget(...keys);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < values.length; i++) {
if (values[i] === null) {
// eslint-disable-next-line no-await-in-loop
await removeActiveConnection(projectId, identityId, connections[i]);
}
}
return redis.llen(set);
};
const onDisconnect = async (client: EventStreamClient) => {
try {
client.close();
clients.delete(client);
await removeActiveConnection(client.auth.projectId, client.auth.actorId, client.id);
} catch (error) {
logger.error(error, "Error during SSE stream disconnection");
}
};
function filterEventsForClient(client: EventStreamClient, event: EventData, registered: RegisteredEvent[]) {
const eventType = toBusEventName(event.data.eventType);
const match = registered.find((r) => r.event === eventType);
if (!match) return;
const item = event.data.payload;
if (Array.isArray(item)) {
if (item.length === 0) return;
const baseSubject = {
eventType,
environment: undefined as string | undefined,
secretPath: undefined as string | undefined
};
const filtered = item.filter((ev) => {
baseSubject.secretPath = ev.secretPath ?? "/";
baseSubject.environment = ev.environment;
return client.matcher.can("subscribe", subject(event.type, baseSubject));
});
if (filtered.length === 0) return;
return client.send({
...event,
data: {
...event.data,
payload: filtered
}
});
}
// For single item
const baseSubject = {
eventType,
secretPath: item.secretPath ?? "/",
environment: item.environment
};
if (client.matcher.can("subscribe", subject(event.type, baseSubject))) {
client.send(event);
}
}
const subscribe = async (
opts: IEventStreamClientOpts & {
onClose?: () => void;
}
) => {
const client = createEventStreamClient(redis, opts);
// Set up event listener on event bus
const unsubscribe = bus.subscribe((event) => {
if (event.type !== opts.type) return;
filterEventsForClient(client, event, opts.registered);
});
client.stream.on("close", () => {
unsubscribe();
void onDisconnect(client); // This will never throw
});
await client.open();
clients.add(client);
return client;
};
const close = () => {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
if (refreshInterval) {
clearInterval(refreshInterval);
}
for (const client of clients) {
client.close();
}
clients.clear();
};
return { subscribe, close, getActiveConnectionsCount };
};
export type TServerSentEventsService = ReturnType<typeof sseServiceFactory>;

View File

@@ -0,0 +1,187 @@
/* eslint-disable no-underscore-dangle */
import { Readable } from "node:stream";
import { MongoAbility, PureAbility } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js";
import Redis from "ioredis";
import { nanoid } from "nanoid";
import { ProjectType } from "@app/db/schemas";
import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission";
import { KeyStorePrefixes } from "@app/keystore/keystore";
import { conditionsMatcher } from "@app/lib/casl";
import { logger } from "@app/lib/logger";
import { EventData, RegisteredEvent } from "./types";
export const getServerSentEventsHeaders = () =>
({
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
Connection: "keep-alive",
"X-Accel-Buffering": "no"
}) as const;
type TAuthInfo = {
actorId: string;
projectId: string;
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
};
export interface IEventStreamClientOpts {
type: ProjectType;
registered: RegisteredEvent[];
onAuthRefresh: (info: TAuthInfo) => Promise<void> | void;
getAuthInfo: () => Promise<TAuthInfo> | TAuthInfo;
}
interface EventMessage {
time?: string | number;
type: string;
data?: unknown;
}
function serializeSseEvent(chunk: EventMessage): string {
let payload = "";
if (chunk.time) payload += `id: ${chunk.time}\n`;
if (chunk.type) payload += `event: ${chunk.type}\n`;
if (chunk.data) payload += `data: ${JSON.stringify(chunk)}\n`;
return `${payload}\n`;
}
export type EventStreamClient = {
id: string;
stream: Readable;
open: () => Promise<void>;
send: (data: EventMessage | EventData) => void;
ping: () => Promise<void>;
refresh: () => Promise<void>;
close: () => void;
get auth(): TAuthInfo;
signal: AbortSignal;
abort: () => void;
matcher: PureAbility;
};
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
const rules = options.registered.map((r) => {
const secretPath = r.conditions?.secretPath;
const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath;
return {
subject: options.type,
action: "subscribe",
conditions: {
eventType: r.event,
...(hasConditions
? {
environment: r.conditions?.environmentSlug ?? "",
secretPath: { $glob: secretPath }
}
: {})
}
};
});
const id = `sse-${nanoid()}`;
const control = new AbortController();
const matcher = new PureAbility(rules, { conditionsMatcher });
let auth: TAuthInfo | undefined;
const stream = new Readable({
objectMode: true
});
// We will manually push data to the stream
stream._read = () => {};
const send = (data: EventMessage | EventData) => {
const chunk = serializeSseEvent(data);
if (!stream.push(chunk)) {
logger.debug("Backpressure detected: dropped manual event");
}
};
stream.on("error", (error: Error) => stream.destroy(error));
const open = async () => {
auth = await options.getAuthInfo();
await options.onAuthRefresh(auth);
const { actorId, projectId } = auth;
const set = KeyStorePrefixes.ActiveSSEConnectionsSet(projectId, actorId);
const key = KeyStorePrefixes.ActiveSSEConnections(projectId, actorId, id);
await Promise.all([redis.rpush(set, id), redis.set(key, "1", "EX", 60)]);
};
const ping = async () => {
if (!auth) return; // Avoid race condition if ping is called before open
const { actorId, projectId } = auth;
const key = KeyStorePrefixes.ActiveSSEConnections(projectId, actorId, id);
await redis.set(key, "1", "EX", 60);
stream.push("1");
};
const close = () => {
if (stream.closed) return;
stream.push(null);
stream.destroy();
};
/**
* Refreshes the connection's auth permissions
* Must be called atleast once when connection is opened
*/
const refresh = async () => {
try {
auth = await options.getAuthInfo();
await options.onAuthRefresh(auth);
} catch (error) {
if (error instanceof Error) {
send({
type: "error",
data: {
...error
}
});
return close();
}
stream.emit("error", error);
}
};
const abort = () => {
try {
control.abort();
} catch (error) {
logger.debug(error, "Error aborting SSE stream");
}
};
return {
id,
stream,
open,
send,
ping,
refresh,
close,
signal: control.signal,
abort,
matcher,
get auth() {
if (!auth) {
throw new Error("Auth info not set");
}
return auth;
}
};
}

View File

@@ -0,0 +1,125 @@
import { z } from "zod";
import { ProjectType } from "@app/db/schemas";
import { Event, EventType } from "@app/ee/services/audit-log/audit-log-types";
export enum TopicName {
CoreServers = "infisical::core-servers"
}
export enum BusEventName {
CreateSecret = "secret:create",
UpdateSecret = "secret:update",
DeleteSecret = "secret:delete"
}
type PublisableEventTypes =
| EventType.CREATE_SECRET
| EventType.CREATE_SECRETS
| EventType.DELETE_SECRET
| EventType.DELETE_SECRETS
| EventType.UPDATE_SECRETS
| EventType.UPDATE_SECRET;
export function toBusEventName(input: EventType) {
switch (input) {
case EventType.CREATE_SECRET:
case EventType.CREATE_SECRETS:
return BusEventName.CreateSecret;
case EventType.UPDATE_SECRET:
case EventType.UPDATE_SECRETS:
return BusEventName.UpdateSecret;
case EventType.DELETE_SECRET:
case EventType.DELETE_SECRETS:
return BusEventName.DeleteSecret;
default:
return null;
}
}
const isBulkEvent = (event: Event): event is Extract<Event, { metadata: { secrets: Array<unknown> } }> => {
return event.type.endsWith("-secrets"); // Feels so wrong
};
export const toPublishableEvent = (event: Event) => {
const name = toBusEventName(event.type);
if (!name) return null;
const e = event as Extract<Event, { type: PublisableEventTypes }>;
if (isBulkEvent(e)) {
return {
name,
isBulk: true,
data: {
eventType: e.type,
payload: e.metadata.secrets.map((s) => ({
environment: e.metadata.environment,
secretPath: e.metadata.secretPath,
...s
}))
}
} as const;
}
return {
name,
isBulk: false,
data: {
eventType: e.type,
payload: {
...e.metadata,
environment: e.metadata.environment
}
}
} as const;
};
export const EventName = z.nativeEnum(BusEventName);
const EventSecretPayload = z.object({
secretPath: z.string().optional(),
secretId: z.string(),
secretKey: z.string(),
environment: z.string()
});
export type EventSecret = z.infer<typeof EventSecretPayload>;
export const EventSchema = z.object({
datacontenttype: z.literal("application/json").optional().default("application/json"),
type: z.nativeEnum(ProjectType),
source: z.string(),
time: z
.string()
.optional()
.default(() => new Date().toISOString()),
data: z.discriminatedUnion("eventType", [
z.object({
specversion: z.number().optional().default(1),
eventType: z.enum([EventType.CREATE_SECRET, EventType.UPDATE_SECRET, EventType.DELETE_SECRET]),
payload: EventSecretPayload
}),
z.object({
specversion: z.number().optional().default(1),
eventType: z.enum([EventType.CREATE_SECRETS, EventType.UPDATE_SECRETS, EventType.DELETE_SECRETS]),
payload: EventSecretPayload.array()
})
// Add more event types as needed
])
});
export type EventData = z.infer<typeof EventSchema>;
export const EventRegisterSchema = z.object({
event: EventName,
conditions: z
.object({
secretPath: z.string().optional().default("/"),
environmentSlug: z.string()
})
.optional()
});
export type RegisteredEvent = z.infer<typeof EventRegisterSchema>;

View File

@@ -1,6 +1,6 @@
import { Knex } from "knex";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
@@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
for await (const projectId of projectIds) {
const project = await projectDAL.findById(projectId, tx);
if (!project) {
throw new NotFoundError({
message: `Failed to find project with ID '${projectId}'`
});
}
if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) {
// eslint-disable-next-line no-continue
continue;
}
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
if (usersToAddProjectKeyFor.length) {
@@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({
});
}
if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's public key in project with ID '${projectId}'`
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) {
@@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
});
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
if (!user.publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with ID '${projectId}'`
});
}
const { ciphertext: encryptedKey, nonce } = crypto
.encryption()
.asymmetric()

View File

@@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
TUserGroupMembershipDALFactory,
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;

View File

@@ -65,7 +65,7 @@ export type TAddUsersToGroup = {
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx: Knex;
};
@@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = {
orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
@@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};

View File

@@ -0,0 +1,83 @@
/* eslint-disable no-case-declarations */
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { buildFindFilter, ormify } from "@app/lib/knex";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
export type TIdentityAuthTemplateDALFactory = ReturnType<typeof identityAuthTemplateDALFactory>;
export const identityAuthTemplateDALFactory = (db: TDbClient) => {
const identityAuthTemplateOrm = ormify(db, TableName.IdentityAuthTemplate);
const findByOrgId = async (
orgId: string,
{ limit, offset, search, tx }: { limit?: number; offset?: number; search?: string; tx?: Knex } = {}
) => {
let query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
let countQuery = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
if (search) {
const searchFilter = `%${search.toLowerCase()}%`;
query = query.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
countQuery = countQuery.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
}
query = query.orderBy("createdAt", "desc");
if (limit !== undefined) {
query = query.limit(limit);
}
if (offset !== undefined) {
query = query.offset(offset);
}
const docs = await query;
const [{ count }] = (await countQuery.count("* as count")) as [{ count: string | number }];
return { docs, totalCount: Number(count) };
};
const findByAuthMethod = async (authMethod: string, orgId: string, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate)
.where({ authMethod, orgId })
.orderBy("createdAt", "desc");
const docs = await query;
return docs;
};
const findTemplateUsages = async (templateId: string, authMethod: string, tx?: Knex) => {
switch (authMethod) {
case IdentityAuthTemplateMethod.LDAP:
const query = (tx || db.replicaNode())(TableName.IdentityLdapAuth)
.join(TableName.Identity, `${TableName.IdentityLdapAuth}.identityId`, `${TableName.Identity}.id`)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ templateId }, TableName.IdentityLdapAuth))
.select(
db.ref("identityId").withSchema(TableName.IdentityLdapAuth),
db.ref("name").withSchema(TableName.Identity).as("identityName")
);
const docs = await query;
return docs;
default:
return [];
}
};
const findByIdAndOrgId = async (id: string, orgId: string, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ id, orgId });
const doc = await query;
return doc?.[0];
};
return {
...identityAuthTemplateOrm,
findByOrgId,
findByAuthMethod,
findTemplateUsages,
findByIdAndOrgId
};
};

View File

@@ -0,0 +1,22 @@
export enum IdentityAuthTemplateMethod {
LDAP = "ldap"
}
export const TEMPLATE_VALIDATION_MESSAGES = {
TEMPLATE_NAME_REQUIRED: "Template name is required",
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
AUTH_METHOD_REQUIRED: "Auth method is required",
TEMPLATE_ID_REQUIRED: "Template ID is required",
LDAP: {
URL_REQUIRED: "LDAP URL is required",
BIND_DN_REQUIRED: "Bind DN is required",
BIND_PASSWORD_REQUIRED: "Bind password is required",
SEARCH_BASE_REQUIRED: "Search base is required"
}
} as const;
export const TEMPLATE_SUCCESS_MESSAGES = {
CREATED: "Template created successfully",
UPDATED: "Template updated successfully",
DELETED: "Template deleted successfully"
} as const;

View File

@@ -0,0 +1,454 @@
import { ForbiddenError } from "@casl/ability";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
import {
TDeleteIdentityAuthTemplateDTO,
TFindTemplateUsagesDTO,
TGetIdentityAuthTemplateDTO,
TGetTemplatesByAuthMethodDTO,
TLdapTemplateFields,
TListIdentityAuthTemplatesDTO,
TUnlinkTemplateUsageDTO
} from "./identity-auth-template-types";
type TIdentityAuthTemplateServiceFactoryDep = {
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
identityLdapAuthDAL: TIdentityLdapAuthDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
export const identityAuthTemplateServiceFactory = ({
identityAuthTemplateDAL,
identityLdapAuthDAL,
permissionService,
kmsService,
licenseService,
auditLogService
}: TIdentityAuthTemplateServiceFactoryDep) => {
// Plan check
const $checkPlan = async (orgId: string) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.machineIdentityAuthTemplates)
throw new BadRequestError({
message:
"Failed to use identity auth template due to plan restriction. Upgrade plan to access machine identity auth templates."
});
};
const createTemplate = async ({
name,
authMethod,
templateFields,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
name: string;
authMethod: string;
templateFields: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const template = await identityAuthTemplateDAL.create({
name,
authMethod,
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob,
orgId: actorOrgId
});
return { ...template, templateFields };
};
const updateTemplate = async ({
templateId,
name,
templateFields,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
templateId: string;
name?: string;
templateFields?: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
let finalTemplateFields: Record<string, unknown> = {};
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
const authTemplate = await identityAuthTemplateDAL.updateById(
templateId,
{
name,
...(templateFields && {
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob
})
},
tx
);
if (templateFields && template.authMethod === IdentityAuthTemplateMethod.LDAP) {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
const currentTemplateFields = JSON.parse(
decryptor({ cipherTextBlob: template.templateFields }).toString()
) as TLdapTemplateFields;
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
finalTemplateFields = mergedTemplateFields;
const ldapUpdateData: {
url?: string;
searchBase?: string;
encryptedBindDN?: Buffer;
encryptedBindPass?: Buffer;
encryptedLdapCaCertificate?: Buffer;
} = {};
if ("url" in templateFields) {
ldapUpdateData.url = mergedTemplateFields.url;
}
if ("searchBase" in templateFields) {
ldapUpdateData.searchBase = mergedTemplateFields.searchBase;
}
if ("bindDN" in templateFields) {
ldapUpdateData.encryptedBindDN = encryptor({
plainText: Buffer.from(mergedTemplateFields.bindDN)
}).cipherTextBlob;
}
if ("bindPass" in templateFields) {
ldapUpdateData.encryptedBindPass = encryptor({
plainText: Buffer.from(mergedTemplateFields.bindPass)
}).cipherTextBlob;
}
if ("ldapCaCertificate" in templateFields) {
ldapUpdateData.encryptedLdapCaCertificate = encryptor({
plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "")
}).cipherTextBlob;
}
if (Object.keys(ldapUpdateData).length > 0) {
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx);
await Promise.all(
updatedLdapAuths.map(async (updatedLdapAuth) => {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId: actorOrgId,
event: {
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
metadata: {
identityId: updatedLdapAuth.identityId,
templateId: template.id
}
}
});
})
);
}
}
return authTemplate;
});
return { ...updatedTemplate, templateFields: finalTemplateFields };
};
const deleteTemplate = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TDeleteIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
// Remove template reference from identityLdapAuth records
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx);
await Promise.all(
updatedLdapAuths.map(async (updatedLdapAuth) => {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId: actorOrgId,
event: {
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
metadata: {
identityId: updatedLdapAuth.identityId,
templateId: template.id
}
}
});
})
);
// Delete the template
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
return deletedTpl;
});
return deletedTemplate;
};
const getTemplate = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
const decryptedTemplateFields = decryptor({ cipherTextBlob: template.templateFields }).toString();
return {
...template,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptedTemplateFields)
};
};
const listTemplates = async ({
limit,
offset,
search,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TListIdentityAuthTemplatesDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset, search });
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
return {
totalCount,
templates: docs.map((doc) => ({
...doc,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
}))
};
};
const getTemplatesByAuthMethod = async ({
authMethod,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetTemplatesByAuthMethodDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const docs = await identityAuthTemplateDAL.findByAuthMethod(authMethod, actorOrgId);
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
return docs.map((doc) => ({
...doc,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
}));
};
const findTemplateUsages = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TFindTemplateUsagesDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const docs = await identityAuthTemplateDAL.findTemplateUsages(templateId, template.authMethod);
return docs;
};
const unlinkTemplateUsage = async ({
templateId,
identityIds,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUnlinkTemplateUsageDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
switch (template.authMethod) {
case IdentityAuthTemplateMethod.LDAP:
await identityLdapAuthDAL.update({ $in: { identityId: identityIds }, templateId }, { templateId: null });
break;
default:
break;
}
};
return {
createTemplate,
updateTemplate,
deleteTemplate,
getTemplate,
listTemplates,
getTemplatesByAuthMethod,
findTemplateUsages,
unlinkTemplateUsage
};
};

View File

@@ -0,0 +1,61 @@
import { TProjectPermission } from "@app/lib/types";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
// Method-specific template field types
export type TLdapTemplateFields = {
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
ldapCaCertificate?: string;
};
// Union type for all template field types
export type TTemplateFieldsByMethod = {
[IdentityAuthTemplateMethod.LDAP]: TLdapTemplateFields;
};
// Generic base types that use conditional types for type safety
export type TCreateIdentityAuthTemplateDTO = {
name: string;
authMethod: IdentityAuthTemplateMethod;
templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityAuthTemplateDTO = {
templateId: string;
name?: string;
templateFields?: Partial<TTemplateFieldsByMethod[IdentityAuthTemplateMethod]>;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteIdentityAuthTemplateDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetIdentityAuthTemplateDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TListIdentityAuthTemplatesDTO = {
limit?: number;
offset?: number;
search?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTemplatesByAuthMethodDTO = {
authMethod: string;
} & Omit<TProjectPermission, "projectId">;
export type TFindTemplateUsagesDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUnlinkTemplateUsageDTO = {
templateId: string;
identityIds: string[];
} & Omit<TProjectPermission, "projectId">;
// Specific LDAP types for convenience
export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO;
export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO;

View File

@@ -0,0 +1,6 @@
export type { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
export { identityAuthTemplateDALFactory } from "./identity-auth-template-dal";
export * from "./identity-auth-template-enums";
export type { TIdentityAuthTemplateServiceFactory } from "./identity-auth-template-service";
export { identityAuthTemplateServiceFactory } from "./identity-auth-template-service";
export type * from "./identity-auth-template-types";

View File

@@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = {
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,

View File

@@ -31,7 +31,8 @@ export const getDefaultOnPremFeatures = () => {
caCrl: false,
sshHostGroups: false,
enterpriseSecretSyncs: false,
enterpriseAppConnections: false
enterpriseAppConnections: false,
machineIdentityAuthTemplates: false
};
};

View File

@@ -59,7 +59,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretScanning: false,
enterpriseSecretSyncs: false,
enterpriseAppConnections: false,
fips: false
fips: false,
eventSubscriptions: false,
machineIdentityAuthTemplates: false
});
export const setupLicenseRequestWithStore = (

View File

@@ -5,13 +5,14 @@
// TODO(akhilmhdh): With tony find out the api structure and fill it here
import { ForbiddenError } from "@casl/ability";
import { AxiosError } from "axios";
import { CronJob } from "cron";
import { Knex } from "knex";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { verifyOfflineLicense } from "@app/lib/crypto";
import { NotFoundError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -603,10 +604,22 @@ export const licenseServiceFactory = ({
});
}
const { data } = await licenseServerCloudApi.request.delete(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
);
return data;
try {
const { data } = await licenseServerCloudApi.request.delete(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
);
return data;
} catch (error) {
if (error instanceof AxiosError) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to remove payment method: ${error.response?.data?.message}`
});
}
throw new BadRequestError({
message: "Unable to remove payment method"
});
}
};
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {

View File

@@ -75,7 +75,9 @@ export type TFeatureSet = {
secretScanning: false;
enterpriseSecretSyncs: false;
enterpriseAppConnections: false;
machineIdentityAuthTemplates: false;
fips: false;
eventSubscriptions: false;
};
export type TOrgPlansTableDTO = {

View File

@@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = {
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;

View File

@@ -161,7 +161,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
ProjectPermissionSecretActions.Delete,
ProjectPermissionSecretActions.Subscribe
],
ProjectPermissionSub.Secrets
);
@@ -265,7 +266,8 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
ProjectPermissionSecretActions.Delete,
ProjectPermissionSecretActions.Subscribe
],
ProjectPermissionSub.Secrets
);

View File

@@ -28,6 +28,15 @@ export enum OrgPermissionKmipActions {
Setup = "setup"
}
export enum OrgPermissionMachineIdentityAuthTemplateActions {
ListTemplates = "list-templates",
EditTemplates = "edit-templates",
CreateTemplates = "create-templates",
DeleteTemplates = "delete-templates",
UnlinkTemplates = "unlink-templates",
AttachTemplates = "attach-templates"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
@@ -88,6 +97,7 @@ export enum OrgPermissionSubjects {
Identity = "identity",
Kms = "kms",
AdminConsole = "organization-admin-console",
MachineIdentityAuthTemplate = "machine-identity-auth-template",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
@@ -126,6 +136,7 @@ export type OrgPermissionSet =
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSubjects.MachineIdentityAuthTemplate]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
@@ -237,6 +248,14 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
"Describe what action an entity can take."
)
}),
z.object({
subject: z
.literal(OrgPermissionSubjects.MachineIdentityAuthTemplate)
.describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionMachineIdentityAuthTemplateActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
@@ -350,6 +369,25 @@ const buildAdminPermission = () => {
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
return rules;
@@ -385,6 +423,16 @@ const buildMemberPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
return rules;
};

View File

@@ -36,7 +36,8 @@ export enum ProjectPermissionSecretActions {
ReadValue = "readValue",
Create = "create",
Edit = "edit",
Delete = "delete"
Delete = "delete",
Subscribe = "subscribe"
}
export enum ProjectPermissionCmekActions {
@@ -204,6 +205,7 @@ export type SecretSubjectFields = {
secretPath: string;
secretName?: string;
secretTags?: string[];
eventType?: string;
};
export type SecretFolderSubjectFields = {
@@ -483,7 +485,17 @@ const SecretConditionV2Schema = z
.object({
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
.partial(),
eventType: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();

View File

@@ -59,7 +59,7 @@ type TScimServiceFactoryDep = {
TOrgMembershipDALFactory,
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
>;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick<
TGroupDALFactory,
@@ -579,6 +579,9 @@ export const scimServiceFactory = ({
});
const serverCfg = await getServerCfg();
const hasEmailChanged = email?.toLowerCase() !== membership.email;
const defaultEmailVerified =
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
await userDAL.transaction(async (tx) => {
await userAliasDAL.update(
{
@@ -605,8 +608,7 @@ export const scimServiceFactory = ({
firstName,
email: email?.toLowerCase(),
lastName,
isEmailVerified:
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
isEmailVerified: hasEmailChanged ? defaultEmailVerified : undefined
},
tx
);

View File

@@ -65,10 +65,14 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { scanSecretPolicyViolations } from "../secret-scanning-v2/secret-scanning-v2-fns";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
@@ -276,13 +280,19 @@ export const secretApprovalRequestServiceFactory = ({
) {
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
}
const hasSecretReadAccess = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => {
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: secretPath || "/",
secretTags: tags.map((i) => i.slug)
});
return canRead;
};
let secrets;
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
secretApprovalRequest.folderId
]);
if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
@@ -298,8 +308,8 @@ export const secretApprovalRequestServiceFactory = ({
version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
secretValueHidden: !hasSecretReadAccess,
secretValue: !hasSecretReadAccess
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
: el.secret && el.secret.isRotatedSecret
? undefined
@@ -314,8 +324,12 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.secret.key,
id: el.secret.id,
version: el.secret.version,
secretValueHidden: !hasSecretReadAccess,
secretValue: !hasSecretReadAccess
secretValueHidden: !getHasSecretReadAccess(
secretApprovalRequest.environment,
el.tags,
secretPath?.[0]?.path
),
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
: el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
@@ -330,8 +344,12 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.secretVersion.key,
id: el.secretVersion.id,
version: el.secretVersion.version,
secretValueHidden: !hasSecretReadAccess,
secretValue: !hasSecretReadAccess
secretValueHidden: !getHasSecretReadAccess(
secretApprovalRequest.environment,
el.tags,
secretPath?.[0]?.path
),
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
: el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
@@ -349,7 +367,7 @@ export const secretApprovalRequestServiceFactory = ({
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encryptedSecrets.map((el) => ({
...el,
secretValueHidden: !hasSecretReadAccess,
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
...decryptSecretWithBot(el, botKey),
secret: el.secret
? {
@@ -369,9 +387,6 @@ export const secretApprovalRequestServiceFactory = ({
: undefined
}));
}
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
secretApprovalRequest.folderId
]);
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets };
};
@@ -1412,6 +1427,20 @@ export const secretApprovalRequestServiceFactory = ({
projectId
});
const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations(
projectId,
secretPath,
[
...(data[SecretOperations.Create] || []),
...(data[SecretOperations.Update] || []).filter((el) => el.secretValue)
].map((el) => ({
secretKey: el.secretKey,
secretValue: el.secretValue as string
})),
project.secretDetectionIgnoreValues || []
);
// for created secret approval change
const createdSecrets = data[SecretOperations.Create];
if (createdSecrets && createdSecrets?.length) {

View File

@@ -21,6 +21,8 @@ const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
type AzureErrorResponse = { error: { message: string } };
const EXPIRY_PADDING_IN_DAYS = 3;
const sleep = async () =>
new Promise((resolve) => {
setTimeout(resolve, 1000);
@@ -33,7 +35,8 @@ export const azureClientSecretRotationFactory: TRotationFactory<
const {
connection,
parameters: { objectId, clientId: clientIdParam },
secretsMapping
secretsMapping,
rotationInterval
} = secretRotation;
/**
@@ -50,7 +53,7 @@ export const azureClientSecretRotationFactory: TRotationFactory<
)}-${now.getFullYear()}`;
const endDateTime = new Date();
endDateTime.setFullYear(now.getFullYear() + 5);
endDateTime.setDate(now.getDate() + rotationInterval * 2 + EXPIRY_PADDING_IN_DAYS); // give 72 hour buffer
try {
const { data } = await request.post<AzureAddPasswordResponse>(
@@ -195,6 +198,12 @@ export const azureClientSecretRotationFactory: TRotationFactory<
callback
) => {
const credentials = await $rotateClientSecret();
// 2.5 years as expiry is set to x2 interval for the inactive period of credential
if (rotationInterval > Math.floor(365 * 2.5) - EXPIRY_PADDING_IN_DAYS) {
throw new BadRequestError({ message: "Azure does not support token duration over 5 years" });
}
return callback(credentials);
};

View File

@@ -51,6 +51,7 @@ const baseSecretRotationV2Query = ({
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db
@@ -104,6 +105,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
connectionCreatedAt,
connectionUpdatedAt,
connectionVersion,
connectionGatewayId,
connectionIsPlatformManagedCredentials,
...el
} = secretRotation;
@@ -123,6 +125,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt,
version: connectionVersion,
gatewayId: connectionGatewayId,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
},
folder: {

View File

@@ -18,7 +18,8 @@ import {
TSecretScanningFactoryInitialize,
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization,
TSecretScanningFactoryTeardown
TSecretScanningFactoryTeardown,
TSecretScanningFactoryValidateConfigUpdate
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
@@ -302,6 +303,13 @@ export const BitbucketSecretScanningFactory = () => {
);
};
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
TBitbucketDataSourceInput["config"],
TBitbucketDataSourceWithConnection
> = async () => {
// no validation required
};
return {
initialize,
postInitialization,
@@ -309,6 +317,7 @@ export const BitbucketSecretScanningFactory = () => {
getFullScanPath,
getDiffScanResourcePayload,
getDiffScanFindingsPayload,
teardown
teardown,
validateConfigUpdate
};
};

View File

@@ -20,7 +20,8 @@ import {
TSecretScanningFactoryInitialize,
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryPostInitialization,
TSecretScanningFactoryTeardown
TSecretScanningFactoryTeardown,
TSecretScanningFactoryValidateConfigUpdate
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
@@ -64,7 +65,14 @@ export const GitHubSecretScanningFactory = () => {
};
const teardown: TSecretScanningFactoryTeardown<TGitHubDataSourceWithConnection> = async () => {
// no termination required
// no teardown required
};
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
TGitHubDataSourceInput["config"],
TGitHubDataSourceWithConnection
> = async () => {
// no validation required
};
const listRawResources: TSecretScanningFactoryListRawResources<TGitHubDataSourceWithConnection> = async (
@@ -238,6 +246,7 @@ export const GitHubSecretScanningFactory = () => {
getFullScanPath,
getDiffScanResourcePayload,
getDiffScanFindingsPayload,
teardown
teardown,
validateConfigUpdate
};
};

View File

@@ -0,0 +1,9 @@
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = {
name: "GitLab",
type: SecretScanningDataSource.GitLab,
connection: AppConnection.GitLab
};

View File

@@ -0,0 +1,8 @@
export enum GitLabDataSourceScope {
Project = "project",
Group = "group"
}
export enum GitLabWebHookEvent {
Push = "Push Hook"
}

View File

@@ -0,0 +1,409 @@
import { Camelize, GitbeakerRequestError, GroupHookSchema, ProjectHookSchema } from "@gitbeaker/rest";
import { join } from "path";
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import {
SecretScanningFindingSeverity,
SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
cloneRepository,
convertPatchLineToFileLineNumber,
replaceNonChangesWithNewlines
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
import {
TSecretScanningFactoryGetDiffScanFindingsPayload,
TSecretScanningFactoryGetDiffScanResourcePayload,
TSecretScanningFactoryGetFullScanPath,
TSecretScanningFactoryInitialize,
TSecretScanningFactoryListRawResources,
TSecretScanningFactoryParams,
TSecretScanningFactoryPostInitialization,
TSecretScanningFactoryTeardown,
TSecretScanningFactoryValidateConfigUpdate
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { titleCaseToCamelCase } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { GitLabProjectRegex } from "@app/lib/regex";
import {
getGitLabConnectionClient,
getGitLabInstanceUrl,
TGitLabConnection
} from "@app/services/app-connection/gitlab";
import { GitLabDataSourceScope } from "./gitlab-secret-scanning-enums";
import {
TGitLabDataSourceCredentials,
TGitLabDataSourceInput,
TGitLabDataSourceWithConnection,
TQueueGitLabResourceDiffScan
} from "./gitlab-secret-scanning-types";
const getMainDomain = (instanceUrl: string) => {
const url = new URL(instanceUrl);
const { hostname } = url;
const parts = hostname.split(".");
if (parts.length >= 2) {
return parts.slice(-2).join(".");
}
return hostname;
};
export const GitLabSecretScanningFactory = ({ appConnectionDAL, kmsService }: TSecretScanningFactoryParams) => {
const initialize: TSecretScanningFactoryInitialize<
TGitLabDataSourceInput,
TGitLabConnection,
TGitLabDataSourceCredentials
> = async ({ payload: { config, name }, connection }, callback) => {
const token = alphaNumericNanoId(64);
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const appCfg = getConfig();
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
const project = await client.Projects.show(projectId);
if (!project) {
throw new BadRequestError({ message: `Could not find project with ID ${projectId}.` });
}
let hook: Camelize<ProjectHookSchema>;
try {
hook = await client.ProjectHooks.add(projectId, `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`, {
token,
pushEvents: true,
enableSslVerification: true,
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${name}`
});
} catch (error) {
if (error instanceof GitbeakerRequestError) {
throw new BadRequestError({ message: `${error.message}: ${error.cause?.description ?? "Unknown Error"}` });
}
throw error;
}
try {
return await callback({
credentials: {
token,
hookId: hook.id
}
});
} catch (error) {
try {
await client.ProjectHooks.remove(projectId, hook.id);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
}
// group scope
const { groupId } = config;
const group = await client.Groups.show(groupId);
if (!group) {
throw new BadRequestError({ message: `Could not find group with ID ${groupId}.` });
}
let hook: Camelize<GroupHookSchema>;
try {
hook = await client.GroupHooks.add(groupId, `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`, {
token,
pushEvents: true,
enableSslVerification: true,
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${name}`
});
} catch (error) {
if (error instanceof GitbeakerRequestError) {
throw new BadRequestError({ message: `${error.message}: ${error.cause?.description ?? "Unknown Error"}` });
}
throw error;
}
try {
return await callback({
credentials: {
token,
hookId: hook.id
}
});
} catch (error) {
try {
await client.GroupHooks.remove(groupId, hook.id);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
};
const postInitialization: TSecretScanningFactoryPostInitialization<
TGitLabDataSourceInput,
TGitLabConnection,
TGitLabDataSourceCredentials
> = async ({ connection, dataSourceId, credentials, payload: { config } }) => {
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const appCfg = getConfig();
const hookUrl = `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`;
const { hookId } = credentials;
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
try {
await client.ProjectHooks.edit(projectId, hookId, hookUrl, {
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${dataSourceId}`,
custom_headers: [{ key: "x-data-source-id", value: dataSourceId }]
});
} catch (error) {
try {
await client.ProjectHooks.remove(projectId, hookId);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
return;
}
// group-scope
const { groupId } = config;
try {
await client.GroupHooks.edit(groupId, hookId, hookUrl, {
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
name: `Infisical Secret Scanning - ${dataSourceId}`,
custom_headers: [{ key: "x-data-source-id", value: dataSourceId }]
});
} catch (error) {
try {
await client.GroupHooks.remove(groupId, hookId);
} catch {
// do nothing, just try to clean up webhook
}
throw error;
}
};
const listRawResources: TSecretScanningFactoryListRawResources<TGitLabDataSourceWithConnection> = async (
dataSource
) => {
const { connection, config } = dataSource;
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
const project = await client.Projects.show(projectId);
if (!project) {
throw new BadRequestError({ message: `Could not find project with ID ${projectId}.` });
}
// scott: even though we have this data we want to get potentially updated name
return [
{
name: project.pathWithNamespace,
externalId: project.id.toString(),
type: SecretScanningResource.Project
}
];
}
// group-scope
const { groupId, includeProjects } = config;
const projects = await client.Groups.allProjects(groupId, {
archived: false
});
const filteredProjects: typeof projects = [];
if (!includeProjects || includeProjects.includes("*")) {
filteredProjects.push(...projects);
} else {
filteredProjects.push(...projects.filter((project) => includeProjects.includes(project.pathWithNamespace)));
}
return filteredProjects.map(({ id, pathWithNamespace }) => ({
name: pathWithNamespace,
externalId: id.toString(),
type: SecretScanningResource.Project
}));
};
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TGitLabDataSourceWithConnection> = async ({
dataSource,
resourceName,
tempFolder
}) => {
const { connection } = dataSource;
const instanceUrl = await getGitLabInstanceUrl(connection.credentials.instanceUrl);
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const user = await client.Users.showCurrentUser();
const repoPath = join(tempFolder, "repo.git");
if (!GitLabProjectRegex.test(resourceName)) {
throw new Error("Invalid GitLab project name");
}
await cloneRepository({
cloneUrl: `https://${user.username}:${connection.credentials.accessToken}@${getMainDomain(instanceUrl)}/${resourceName}.git`,
repoPath
});
return repoPath;
};
const teardown: TSecretScanningFactoryTeardown<
TGitLabDataSourceWithConnection,
TGitLabDataSourceCredentials
> = async ({ dataSource: { connection, config }, credentials: { hookId } }) => {
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
if (config.scope === GitLabDataSourceScope.Project) {
const { projectId } = config;
try {
await client.ProjectHooks.remove(projectId, hookId);
} catch (error) {
// do nothing, just try to clean up webhook
}
return;
}
const { groupId } = config;
try {
await client.GroupHooks.remove(groupId, hookId);
} catch (error) {
// do nothing, just try to clean up webhook
}
};
const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<
TQueueGitLabResourceDiffScan["payload"]
> = ({ project }) => {
return {
name: project.path_with_namespace,
externalId: project.id.toString(),
type: SecretScanningResource.Project
};
};
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
TGitLabDataSourceWithConnection,
TQueueGitLabResourceDiffScan["payload"]
> = async ({ dataSource, payload, resourceName, configPath }) => {
const { connection } = dataSource;
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
const { commits, project } = payload;
const allFindings: SecretMatch[] = [];
for (const commit of commits) {
// eslint-disable-next-line no-await-in-loop
const commitDiffs = await client.Commits.showDiff(project.id, commit.id);
for (const commitDiff of commitDiffs) {
// eslint-disable-next-line no-continue
if (commitDiff.deletedFile) continue;
// eslint-disable-next-line no-await-in-loop
const findings = await scanContentAndGetFindings(
replaceNonChangesWithNewlines(`\n${commitDiff.diff}`),
configPath
);
const adjustedFindings = findings.map((finding) => {
const startLine = convertPatchLineToFileLineNumber(commitDiff.diff, finding.StartLine);
const endLine =
finding.StartLine === finding.EndLine
? startLine
: convertPatchLineToFileLineNumber(commitDiff.diff, finding.EndLine);
const startColumn = finding.StartColumn - 1; // subtract 1 for +
const endColumn = finding.EndColumn - 1; // subtract 1 for +
const authorName = commit.author.name;
const authorEmail = commit.author.email;
return {
...finding,
StartLine: startLine,
EndLine: endLine,
StartColumn: startColumn,
EndColumn: endColumn,
File: commitDiff.newPath,
Commit: commit.id,
Author: authorName,
Email: authorEmail,
Message: commit.message,
Fingerprint: `${commit.id}:${commitDiff.newPath}:${finding.RuleID}:${startLine}:${startColumn}`,
Date: commit.timestamp,
Link: `https://gitlab.com/${resourceName}/blob/${commit.id}/${commitDiff.newPath}#L${startLine}`
};
});
allFindings.push(...adjustedFindings);
}
}
return allFindings.map(
({
// discard match and secret as we don't want to store
Match,
Secret,
...finding
}) => ({
details: titleCaseToCamelCase(finding),
fingerprint: finding.Fingerprint,
severity: SecretScanningFindingSeverity.High,
rule: finding.RuleID
})
);
};
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
TGitLabDataSourceInput["config"],
TGitLabDataSourceWithConnection
> = async ({ config, dataSource }) => {
if (dataSource.config.scope !== config.scope) {
throw new BadRequestError({ message: "Cannot change Data Source scope after creation." });
}
};
return {
listRawResources,
getFullScanPath,
initialize,
postInitialization,
teardown,
getDiffScanResourcePayload,
getDiffScanFindingsPayload,
validateConfigUpdate
};
};

View File

@@ -0,0 +1,101 @@
import { z } from "zod";
import { GitLabDataSourceScope } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
import {
SecretScanningDataSource,
SecretScanningResource
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import {
BaseCreateSecretScanningDataSourceSchema,
BaseSecretScanningDataSourceSchema,
BaseSecretScanningFindingSchema,
BaseUpdateSecretScanningDataSourceSchema,
GitRepositoryScanFindingDetailsSchema
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-schemas";
import { SecretScanningDataSources } from "@app/lib/api-docs";
import { GitLabProjectRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const GitLabDataSourceConfigSchema = z.discriminatedUnion("scope", [
z.object({
scope: z.literal(GitLabDataSourceScope.Group).describe(SecretScanningDataSources.CONFIG.GITLAB.scope),
groupId: z.number().describe(SecretScanningDataSources.CONFIG.GITLAB.groupId),
groupName: z.string().trim().max(256).optional().describe(SecretScanningDataSources.CONFIG.GITLAB.groupName),
includeProjects: z
.array(
z
.string()
.min(1)
.max(256)
.refine((value) => value === "*" || GitLabProjectRegex.test(value), "Invalid project name format")
)
.nonempty("One or more projects required")
.max(100, "Cannot configure more than 100 projects")
.default(["*"])
.describe(SecretScanningDataSources.CONFIG.GITLAB.includeProjects)
}),
z.object({
scope: z.literal(GitLabDataSourceScope.Project).describe(SecretScanningDataSources.CONFIG.GITLAB.scope),
projectName: z.string().trim().max(256).optional().describe(SecretScanningDataSources.CONFIG.GITLAB.projectName),
projectId: z.number().describe(SecretScanningDataSources.CONFIG.GITLAB.projectId)
})
]);
export const GitLabDataSourceSchema = BaseSecretScanningDataSourceSchema({
type: SecretScanningDataSource.GitLab,
isConnectionRequired: true
})
.extend({
config: GitLabDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const CreateGitLabDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
type: SecretScanningDataSource.GitLab,
isConnectionRequired: true
})
.extend({
config: GitLabDataSourceConfigSchema
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const UpdateGitLabDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(SecretScanningDataSource.GitLab)
.extend({
config: GitLabDataSourceConfigSchema.optional()
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const GitLabDataSourceListItemSchema = z
.object({
name: z.literal("GitLab"),
connection: z.literal(AppConnection.GitLab),
type: z.literal(SecretScanningDataSource.GitLab)
})
.describe(
JSON.stringify({
title: "GitLab"
})
);
export const GitLabFindingSchema = BaseSecretScanningFindingSchema.extend({
resourceType: z.literal(SecretScanningResource.Project),
dataSourceType: z.literal(SecretScanningDataSource.GitLab),
details: GitRepositoryScanFindingDetailsSchema
});
export const GitLabDataSourceCredentialsSchema = z.object({
token: z.string(),
hookId: z.number()
});

View File

@@ -0,0 +1,94 @@
import { GitLabDataSourceScope } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import {
TGitLabDataSource,
TGitLabDataSourceCredentials,
THandleGitLabPushEvent
} from "./gitlab-secret-scanning-types";
export const gitlabSecretScanningService = (
secretScanningV2DAL: TSecretScanningV2DALFactory,
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const handlePushEvent = async ({ payload, token, dataSourceId }: THandleGitLabPushEvent) => {
if (!payload.total_commits_count || !payload.project) {
logger.warn(
`secretScanningV2PushEvent: GitLab - Insufficient data [changes=${
payload.total_commits_count ?? 0
}] [projectName=${payload.project?.path_with_namespace ?? "unknown"}] [projectId=${payload.project?.id ?? "unknown"}]`
);
return;
}
const dataSource = (await secretScanningV2DAL.dataSources.findOne({
id: dataSourceId,
type: SecretScanningDataSource.GitLab
})) as TGitLabDataSource | undefined;
if (!dataSource) {
logger.error(
`secretScanningV2PushEvent: GitLab - Could not find data source [dataSourceId=${dataSourceId}] [projectId=${payload.project.id}]`
);
return;
}
const { isAutoScanEnabled, config, encryptedCredentials, projectId } = dataSource;
if (!encryptedCredentials) {
logger.info(
`secretScanningV2PushEvent: GitLab - Could not find encrypted credentials [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
return;
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedCredentials = decryptor({ cipherTextBlob: encryptedCredentials });
const credentials = JSON.parse(decryptedCredentials.toString()) as TGitLabDataSourceCredentials;
if (token !== credentials.token) {
logger.error(
`secretScanningV2PushEvent: GitLab - Invalid webhook token [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
return;
}
if (!isAutoScanEnabled) {
logger.info(
`secretScanningV2PushEvent: GitLab - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
return;
}
if (
config.scope === GitLabDataSourceScope.Project
? config.projectId.toString() === payload.project_id.toString()
: config.includeProjects.includes("*") || config.includeProjects.includes(payload.project.path_with_namespace)
) {
await secretScanningV2Queue.queueResourceDiffScan({
dataSourceType: SecretScanningDataSource.GitLab,
payload,
dataSourceId: dataSource.id
});
} else {
logger.info(
`secretScanningV2PushEvent: GitLab - ignoring due to repository not being present in config [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
);
}
};
return {
handlePushEvent
};
};

View File

@@ -0,0 +1,97 @@
import { z } from "zod";
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TGitLabConnection } from "@app/services/app-connection/gitlab";
import {
CreateGitLabDataSourceSchema,
GitLabDataSourceCredentialsSchema,
GitLabDataSourceListItemSchema,
GitLabDataSourceSchema,
GitLabFindingSchema
} from "./gitlab-secret-scanning-schemas";
export type TGitLabDataSource = z.infer<typeof GitLabDataSourceSchema>;
export type TGitLabDataSourceInput = z.infer<typeof CreateGitLabDataSourceSchema>;
export type TGitLabDataSourceListItem = z.infer<typeof GitLabDataSourceListItemSchema>;
export type TGitLabFinding = z.infer<typeof GitLabFindingSchema>;
export type TGitLabDataSourceWithConnection = TGitLabDataSource & {
connection: TGitLabConnection;
};
export type TGitLabDataSourceCredentials = z.infer<typeof GitLabDataSourceCredentialsSchema>;
export type TGitLabDataSourcePushEventPayload = {
object_kind: "push";
event_name: "push";
before: string;
after: string;
ref: string;
ref_protected: boolean;
checkout_sha: string;
user_id: number;
user_name: string;
user_username: string;
user_email: string;
user_avatar: string;
project_id: number;
project: {
id: number;
name: string;
description: string;
web_url: string;
avatar_url: string | null;
git_ssh_url: string;
git_http_url: string;
namespace: string;
visibility_level: number;
path_with_namespace: string;
default_branch: string;
homepage: string;
url: string;
ssh_url: string;
http_url: string;
};
repository: {
name: string;
url: string;
description: string;
homepage: string;
git_http_url: string;
git_ssh_url: string;
visibility_level: number;
};
commits: {
id: string;
message: string;
title: string;
timestamp: string;
url: string;
author: {
name: string;
email: string;
};
added: string[];
modified: string[];
removed: string[];
}[];
total_commits_count: number;
};
export type THandleGitLabPushEvent = {
payload: TGitLabDataSourcePushEventPayload;
dataSourceId: string;
token: string;
};
export type TQueueGitLabResourceDiffScan = {
dataSourceType: SecretScanningDataSource.GitLab;
payload: TGitLabDataSourcePushEventPayload;
dataSourceId: string;
resourceId: string;
scanId: string;
};

View File

@@ -0,0 +1,3 @@
export * from "./gitlab-secret-scanning-constants";
export * from "./gitlab-secret-scanning-schemas";
export * from "./gitlab-secret-scanning-types";

View File

@@ -49,6 +49,7 @@ const baseSecretScanningDataSourceQuery = ({
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db
@@ -82,6 +83,7 @@ const expandSecretScanningDataSource = <
connectionUpdatedAt,
connectionVersion,
connectionIsPlatformManagedCredentials,
connectionGatewayId,
...el
} = dataSource;
@@ -100,7 +102,8 @@ const expandSecretScanningDataSource = <
createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt,
version: connectionVersion,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
gatewayId: connectionGatewayId
}
: undefined
};

View File

@@ -1,6 +1,7 @@
export enum SecretScanningDataSource {
GitHub = "github",
Bitbucket = "bitbucket"
Bitbucket = "bitbucket",
GitLab = "gitlab"
}
export enum SecretScanningScanStatus {

View File

@@ -1,5 +1,6 @@
import { BitbucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory";
import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory";
import { GitLabSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-factory";
import { SecretScanningDataSource } from "./secret-scanning-v2-enums";
import {
@@ -19,5 +20,6 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory<
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation,
[SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation
[SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation,
[SecretScanningDataSource.GitLab]: GitLabSecretScanningFactory as TSecretScanningFactoryImplementation
};

View File

@@ -1,11 +1,22 @@
import { AxiosError } from "axios";
import { exec } from "child_process";
import { join } from "path";
import picomatch from "picomatch";
import RE2 from "re2";
import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import {
createTempFolder,
deleteTempFolder,
readFindingsFile,
writeTextToFile
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github";
import { GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/gitlab";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { titleCaseToCamelCase } from "@app/lib/fn";
import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums";
@@ -13,7 +24,8 @@ import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListIte
const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record<SecretScanningDataSource, TSecretScanningDataSourceListItem> = {
[SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
[SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
[SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
[SecretScanningDataSource.GitLab]: GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
};
export const listSecretScanningDataSourceOptions = () => {
@@ -46,6 +58,19 @@ export function scanDirectory(inputPath: string, outputPath: string, configPath?
});
}
export function scanFile(inputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `infisical scan --exit-code=77 --source "${inputPath}" --no-git`;
exec(command, (error) => {
if (error && error.code === 77) {
reject(error);
} else {
resolve();
}
});
});
}
export const scanGitRepositoryAndGetFindings = async (
scanPath: string,
findingsPath: string,
@@ -140,3 +165,47 @@ export const parseScanErrorMessage = (err: unknown): string => {
? errorMessage
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
};
export const scanSecretPolicyViolations = async (
projectId: string,
secretPath: string,
secrets: { secretKey: string; secretValue: string }[],
ignoreValues: string[]
) => {
const appCfg = getConfig();
if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) {
return;
}
const match = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.find(
(el) => el.projectId === projectId && picomatch.isMatch(secretPath, el.secretPath, { strictSlashes: false })
);
if (!match) {
return;
}
const tempFolder = await createTempFolder();
try {
const scanPromises = secrets
.filter((secret) => !ignoreValues.includes(secret.secretValue))
.map(async (secret) => {
const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`);
await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`);
try {
await scanFile(secretFilePath);
} catch (error) {
throw new BadRequestError({
message: `Secret value detected in ${secret.secretKey}. Please add this instead to the designated secrets path in the project.`,
name: "SecretPolicyViolation"
});
}
});
await Promise.all(scanPromises);
} finally {
await deleteTempFolder(tempFolder);
}
};

View File

@@ -3,15 +3,18 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record<SecretScanningDataSource, string> = {
[SecretScanningDataSource.GitHub]: "GitHub",
[SecretScanningDataSource.Bitbucket]: "Bitbucket"
[SecretScanningDataSource.Bitbucket]: "Bitbucket",
[SecretScanningDataSource.GitLab]: "GitLab"
};
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket,
[SecretScanningDataSource.GitLab]: AppConnection.GitLab
};
export const AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = {
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" },
[SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" }
[SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" },
[SecretScanningDataSource.GitLab]: { verb: "push", noun: "projects" }
};

View File

@@ -16,6 +16,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type";
@@ -48,6 +49,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
};
@@ -62,7 +64,8 @@ export const secretScanningV2QueueServiceFactory = async ({
smtpService,
kmsService,
auditLogService,
keyStore
keyStore,
appConnectionDAL
}: TSecretRotationV2QueueServiceFactoryDep) => {
const queueDataSourceFullScan = async (
dataSource: TSecretScanningDataSourceWithConnection,
@@ -71,7 +74,10 @@ export const secretScanningV2QueueServiceFactory = async ({
try {
const { type } = dataSource;
const factory = SECRET_SCANNING_FACTORY_MAP[type]();
const factory = SECRET_SCANNING_FACTORY_MAP[type]({
kmsService,
appConnectionDAL
});
const rawResources = await factory.listRawResources(dataSource);
@@ -171,7 +177,10 @@ export const secretScanningV2QueueServiceFactory = async ({
let connection: TAppConnection | null = null;
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const findingsPath = join(tempFolder, "findings.json");
@@ -329,7 +338,10 @@ export const secretScanningV2QueueServiceFactory = async ({
dataSourceId,
dataSourceType
}: Pick<TQueueSecretScanningResourceDiffScan, "payload" | "dataSourceId" | "dataSourceType">) => {
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]();
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const resourcePayload = factory.getDiffScanResourcePayload(payload);
@@ -391,7 +403,10 @@ export const secretScanningV2QueueServiceFactory = async ({
if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const tempFolder = await createTempFolder();

View File

@@ -46,6 +46,7 @@ import {
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
@@ -53,12 +54,14 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { bitbucketSecretScanningService } from "./bitbucket/bitbucket-secret-scanning-service";
import { gitlabSecretScanningService } from "./gitlab/gitlab-secret-scanning-service";
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue";
export type TSecretScanningV2ServiceFactoryDep = {
secretScanningV2DAL: TSecretScanningV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretScanningV2Queue: Pick<
@@ -76,6 +79,7 @@ export const secretScanningV2ServiceFactory = ({
appConnectionService,
licenseService,
secretScanningV2Queue,
appConnectionDAL,
kmsService
}: TSecretScanningV2ServiceFactoryDep) => {
const $checkListSecretScanningDataSourcesByProjectIdPermissions = async (
@@ -255,7 +259,10 @@ export const secretScanningV2ServiceFactory = ({
);
}
const factory = SECRET_SCANNING_FACTORY_MAP[payload.type]();
const factory = SECRET_SCANNING_FACTORY_MAP[payload.type]({
appConnectionDAL,
kmsService
});
try {
const createdDataSource = await factory.initialize(
@@ -363,6 +370,31 @@ export const secretScanningV2ServiceFactory = ({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
let connection: TAppConnection | null = null;
if (dataSource.connectionId) {
// validates permission to connect and app is valid for data source
connection = await appConnectionService.connectAppConnectionById(
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSource.type],
dataSource.connectionId,
actor
);
}
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type]({
appConnectionDAL,
kmsService
});
if (payload.config) {
await factory.validateConfigUpdate({
dataSource: {
...dataSource,
connection
} as TSecretScanningDataSourceWithConnection,
config: payload.config as TSecretScanningDataSourceWithConnection["config"]
});
}
try {
const updatedDataSource = await secretScanningV2DAL.dataSources.updateById(dataSourceId, payload);
@@ -416,7 +448,10 @@ export const secretScanningV2ServiceFactory = ({
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
});
const factory = SECRET_SCANNING_FACTORY_MAP[type]();
const factory = SECRET_SCANNING_FACTORY_MAP[type]({
appConnectionDAL,
kmsService
});
let connection: TAppConnection | null = null;
if (dataSource.connection) {
@@ -903,6 +938,7 @@ export const secretScanningV2ServiceFactory = ({
findSecretScanningConfigByProjectId,
upsertSecretScanningConfig,
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService),
gitlab: gitlabSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
};
};

View File

@@ -21,14 +21,25 @@ import {
TGitHubFinding,
TQueueGitHubResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/github";
import {
TGitLabDataSource,
TGitLabDataSourceCredentials,
TGitLabDataSourceInput,
TGitLabDataSourceListItem,
TGitLabDataSourceWithConnection,
TGitLabFinding,
TQueueGitLabResourceDiffScan
} from "@app/ee/services/secret-scanning-v2/gitlab";
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
import {
SecretScanningDataSource,
SecretScanningFindingStatus,
SecretScanningScanStatus
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource | TGitLabDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt?: Date | null;
@@ -52,15 +63,25 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & {
export type TSecretScanningDataSourceWithConnection =
| TGitHubDataSourceWithConnection
| TBitbucketDataSourceWithConnection;
| TBitbucketDataSourceWithConnection
| TGitLabDataSourceWithConnection;
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitbucketDataSourceInput;
export type TSecretScanningDataSourceInput =
| TGitHubDataSourceInput
| TBitbucketDataSourceInput
| TGitLabDataSourceInput;
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitbucketDataSourceListItem;
export type TSecretScanningDataSourceListItem =
| TGitHubDataSourceListItem
| TBitbucketDataSourceListItem
| TGitLabDataSourceListItem;
export type TSecretScanningDataSourceCredentials = TBitbucketDataSourceCredentials | undefined;
export type TSecretScanningDataSourceCredentials =
| TBitbucketDataSourceCredentials
| TGitLabDataSourceCredentials
| undefined;
export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding;
export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding | TGitLabFinding;
export type TListSecretScanningDataSourcesByProjectId = {
projectId: string;
@@ -112,7 +133,10 @@ export type TQueueSecretScanningDataSourceFullScan = {
scanId: string;
};
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan;
export type TQueueSecretScanningResourceDiffScan =
| TQueueGitHubResourceDiffScan
| TQueueBitbucketResourceDiffScan
| TQueueGitLabResourceDiffScan;
export type TQueueSecretScanningSendNotification = {
dataSource: TSecretScanningDataSources;
@@ -170,6 +194,11 @@ export type TSecretScanningFactoryInitialize<
callback: (parameters: { credentials?: C; externalId?: string }) => Promise<TSecretScanningDataSourceRaw>
) => Promise<TSecretScanningDataSourceRaw>;
export type TSecretScanningFactoryValidateConfigUpdate<
C extends TSecretScanningDataSourceInput["config"],
T extends TSecretScanningDataSourceWithConnection
> = (params: { config: C; dataSource: T }) => Promise<void>;
export type TSecretScanningFactoryPostInitialization<
P extends TSecretScanningDataSourceInput,
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
@@ -181,17 +210,23 @@ export type TSecretScanningFactoryTeardown<
C extends TSecretScanningDataSourceCredentials = undefined
> = (params: { dataSource: T; credentials: C }) => Promise<void>;
export type TSecretScanningFactoryParams = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSecretScanningFactory<
T extends TSecretScanningDataSourceWithConnection,
P extends TQueueSecretScanningResourceDiffScan["payload"],
I extends TSecretScanningDataSourceInput,
C extends TSecretScanningDataSourceCredentials | undefined = undefined
> = () => {
> = (params: TSecretScanningFactoryParams) => {
listRawResources: TSecretScanningFactoryListRawResources<T>;
getFullScanPath: TSecretScanningFactoryGetFullScanPath<T>;
initialize: TSecretScanningFactoryInitialize<I, T["connection"] | undefined, C>;
postInitialization: TSecretScanningFactoryPostInitialization<I, T["connection"] | undefined, C>;
teardown: TSecretScanningFactoryTeardown<T, C>;
validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<I["config"], T>;
getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<P>;
getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<T, P>;
};

View File

@@ -2,10 +2,12 @@ import { z } from "zod";
import { BitbucketDataSourceSchema, BitbucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github";
import { GitLabDataSourceSchema, GitLabFindingSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema,
BitbucketDataSourceSchema
BitbucketDataSourceSchema,
GitLabDataSourceSchema
]);
export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [
@@ -18,5 +20,10 @@ export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType"
JSON.stringify({
title: "Bitbucket"
})
),
GitLabFindingSchema.describe(
JSON.stringify({
title: "GitLab"
})
)
]);

View File

@@ -46,7 +46,11 @@ export const KeyStorePrefixes = {
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`,
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`,
ActiveSSEConnectionsSet: (projectId: string, identityId: string) =>
`sse-connections:${projectId}:${identityId}` as const,
ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) =>
`sse-connections:${projectId}:${identityId}:${connectionId}` as const
};
export const KeyStoreTtls = {

View File

@@ -18,6 +18,7 @@ import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/
export enum ApiDocsTags {
Identities = "Identities",
IdentityTemplates = "Identity Templates",
TokenAuth = "Token Auth",
UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth",
@@ -69,7 +70,8 @@ export enum ApiDocsTags {
SecretScanning = "Secret Scanning",
OidcSso = "OIDC SSO",
SamlSso = "SAML SSO",
LdapSso = "LDAP SSO"
LdapSso = "LDAP SSO",
Events = "Event Subscriptions"
}
export const GROUPS = {
@@ -214,6 +216,7 @@ export const LDAP_AUTH = {
password: "The password of the LDAP user to login."
},
ATTACH: {
templateId: "The ID of the identity auth template to attach the configuration onto.",
identityId: "The ID of the identity to attach the configuration onto.",
url: "The URL of the LDAP server.",
allowedFields:
@@ -240,7 +243,8 @@ export const LDAP_AUTH = {
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
templateId: "The ID of the identity auth template to update the configuration to."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the configuration for."
@@ -664,6 +668,10 @@ export const ORGANIZATIONS = {
organizationId: "The ID of the organization to delete the membership from.",
membershipId: "The ID of the membership to delete."
},
BULK_DELETE_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to delete the memberships from.",
membershipIds: "The IDs of the memberships to delete."
},
LIST_IDENTITY_MEMBERSHIPS: {
orgId: "The ID of the organization to get identity memberships from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
@@ -704,7 +712,8 @@ export const PROJECTS = {
hasDeleteProtection: "Enable or disable delete protection for the project.",
secretSharing: "Enable or disable secret sharing for the project.",
showSnapshotsLegacy: "Enable or disable legacy snapshots for the project.",
defaultProduct: "The default product in which the project will open"
defaultProduct: "The default product in which the project will open",
secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection."
},
GET_KEY: {
workspaceId: "The ID of the project to get the key from."
@@ -2252,7 +2261,9 @@ export const AppConnections = {
AZURE_DEVOPS: {
code: "The OAuth code to use to connect with Azure DevOps.",
tenantId: "The Tenant ID to use to connect with Azure DevOps.",
orgName: "The Organization name to use to connect with Azure DevOps."
orgName: "The Organization name to use to connect with Azure DevOps.",
clientId: "The Client ID to use to connect with Azure Client Secrets.",
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
},
OCI: {
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
@@ -2295,6 +2306,9 @@ export const AppConnections = {
DIGITAL_OCEAN_APP_PLATFORM: {
apiToken: "The API token used to authenticate with Digital Ocean App Platform."
},
NETLIFY: {
accessToken: "The Access token used to authenticate with Netlify."
},
OKTA: {
instanceUrl: "The URL used to access your Okta organization.",
apiToken: "The API token used to authenticate with Okta."
@@ -2399,12 +2413,18 @@ export const SecretSyncs = {
env: "The name of the GitHub environment."
},
AZURE_KEY_VAULT: {
vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/"
vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
clientId: "The Client ID to use to connect with Azure Client Secrets.",
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
},
AZURE_APP_CONFIGURATION: {
configurationUrl:
"The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/",
label: "An optional label to assign to secrets created in Azure App Configuration."
label: "An optional label to assign to secrets created in Azure App Configuration.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
clientId: "The Client ID to use to connect with Azure Client Secrets.",
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
},
AZURE_DEVOPS: {
devopsProjectId: "The ID of the Azure DevOps project to sync secrets to.",
@@ -2471,6 +2491,7 @@ export const SecretSyncs = {
},
RENDER: {
serviceId: "The ID of the Render service to sync secrets to.",
environmentGroupId: "The ID of the Render environment group to sync secrets to.",
scope: "The Render scope that secrets should be synced to.",
type: "The Render resource type to sync secrets to."
},
@@ -2520,6 +2541,13 @@ export const SecretSyncs = {
workspaceSlug: "The Bitbucket Workspace slug to sync secrets to.",
repositorySlug: "The Bitbucket Repository slug to sync secrets to.",
environmentId: "The Bitbucket Deployment Environment uuid to sync secrets to."
},
NETLIFY: {
accountId: "The ID of the Netlify account to sync secrets to.",
accountName: "The name of the Netlify account to sync secrets to.",
siteName: "The name of the Netlify site to sync secrets to.",
siteId: "The ID of the Netlify site to sync secrets to.",
context: "The Netlify context to sync secrets to."
}
}
};
@@ -2701,6 +2729,14 @@ export const SecretScanningDataSources = {
GITHUB: {
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
},
GITLAB: {
includeProjects: 'The projects to include when scanning. Defaults to all projects (["*"]).',
scope: "The GitLab scope scanning should occur at (project or group level).",
projectId: "The ID of the project to scan.",
projectName: "The name of the project to scan.",
groupId: "The ID of the group to scan projects from.",
groupName: "The name of the group to scan projects from."
},
BITBUCKET: {
workspaceSlug: "The workspace to scan.",
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
@@ -2838,3 +2874,10 @@ export const LdapSso = {
caCert: "The CA certificate to use when verifying the LDAP server certificate."
}
};
export const EventSubscriptions = {
SUBSCRIBE_PROJECT_EVENTS: {
projectId: "The ID of the project to subscribe to events for.",
register: "List of events you want to subscribe to"
}
};

View File

@@ -59,6 +59,7 @@ const envSchema = z
AUDIT_LOGS_DB_ROOT_CERT: zpStr(
z.string().describe("Postgres database base64-encoded CA cert for Audit logs").optional()
),
DISABLE_AUDIT_LOG_STORAGE: zodStrBool.default("false").optional().describe("Disable audit log storage"),
MAX_LEASE_LIMIT: z.coerce.number().default(10000),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
@@ -204,6 +205,17 @@ const envSchema = z
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
// Special Detection Feature
PARAMS_FOLDER_SECRET_DETECTION_PATHS: zpStr(
z
.string()
.optional()
.transform((val) => {
if (!val) return undefined;
return JSON.parse(val) as { secretPath: string; projectId: string }[];
})
),
// HSM
HSM_LIB_PATH: zpStr(z.string().optional()),
HSM_PIN: zpStr(z.string().optional()),
@@ -358,6 +370,7 @@ const envSchema = z
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","),
PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0,
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID:
data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET:
@@ -470,6 +483,15 @@ export const overwriteSchema: {
fields: { key: keyof TEnvConfig; description?: string }[];
};
} = {
auditLogs: {
name: "Audit Logs",
fields: [
{
key: "DISABLE_AUDIT_LOG_STORAGE",
description: "Disable audit log storage"
}
]
},
aws: {
name: "AWS",
fields: [
@@ -484,7 +506,7 @@ export const overwriteSchema: {
]
},
azureAppConfiguration: {
name: "Azure App Configuration",
name: "Azure App Connection: App Configuration",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID",
@@ -497,7 +519,7 @@ export const overwriteSchema: {
]
},
azureKeyVault: {
name: "Azure Key Vault",
name: "Azure App Connection: Key Vault",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID",
@@ -510,7 +532,7 @@ export const overwriteSchema: {
]
},
azureClientSecrets: {
name: "Azure Client Secrets",
name: "Azure App Connection: Client Secrets",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID",
@@ -523,7 +545,7 @@ export const overwriteSchema: {
]
},
azureDevOps: {
name: "Azure DevOps",
name: "Azure App Connection: DevOps",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID",

View File

@@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>
type TLatestKey = TProjectKeys & {
sender: {
publicKey: string;
publicKey?: string;
};
};
@@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s
return results;
};
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
@@ -143,6 +147,10 @@ export const decryptSecretVersions = (
privateKey: string,
latestKey: TLatestKey
) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
@@ -195,6 +203,10 @@ export const decryptSecretApprovals = (
privateKey: string,
latestKey: TLatestKey
) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
@@ -247,6 +259,10 @@ export const decryptIntegrationAuths = (
privateKey: string,
latestKey: TLatestKey
) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,

View File

@@ -4,6 +4,7 @@ import jsrp from "jsrp";
import { TUserEncryptionKeys } from "@app/db/schemas";
import { UserEncryption } from "@app/services/user/user-types";
import { BadRequestError } from "../errors";
import { crypto, SymmetricKeySize } from "./cryptography";
export const generateSrpServerKey = async (salt: string, verifier: string) => {
@@ -127,6 +128,10 @@ export const getUserPrivateKey = async (
>
) => {
if (user.encryptionVersion === UserEncryption.V1) {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
return crypto
.encryption()
.symmetric()
@@ -138,12 +143,25 @@ export const getUserPrivateKey = async (
keySize: SymmetricKeySize.Bits128
});
}
// still used for legacy things
if (
user.encryptionVersion === UserEncryption.V2 &&
user.protectedKey &&
user.protectedKeyIV &&
user.protectedKeyTag
) {
if (
!user.salt ||
!user.protectedKey ||
!user.protectedKeyIV ||
!user.protectedKeyTag ||
!user.encryptedPrivateKey ||
!user.iv ||
!user.tag
) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,

View File

@@ -11,3 +11,5 @@ export const UserPrincipalNameRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._-]
export const LdapUrlRegex = new RE2(/^ldaps?:\/\//);
export const BasicRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/);
export const GitLabProjectRegex = new RE2(/^[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)+$/);

View File

@@ -14,6 +14,11 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
if (appCfg.isDevelopmentMode) return;
const validUrl = new URL(url);
if (validUrl.username || validUrl.password) {
throw new BadRequestError({ message: "URLs with user credentials (e.g., user:pass@) are not allowed" });
}
const inputHostIps: string[] = [];
if (isIPv4(validUrl.hostname)) {
inputHostIps.push(validUrl.hostname);

View File

@@ -22,6 +22,7 @@ import { crypto } from "@app/lib/crypto";
import { logger } from "@app/lib/logger";
import { QueueWorkerProfile } from "@app/lib/types";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { ExternalPlatforms } from "@app/services/external-migration/external-migration-types";
import {
TFailedIntegrationSyncEmailsPayload,
TIntegrationSyncPayload,
@@ -228,6 +229,7 @@ export type TQueueJobTypes = {
name: QueueJobs.ImportSecretsFromExternalSource;
payload: {
actorEmail: string;
importType: ExternalPlatforms;
data: {
iv: string;
tag: string;

View File

@@ -22,6 +22,7 @@ export type TAuthMode =
orgId: string;
authMethod: AuthMethod;
isMfaVerified?: boolean;
token: AuthModeJwtTokenPayload;
}
| {
authMode: AuthMode.API_KEY;
@@ -30,6 +31,7 @@ export type TAuthMode =
userId: string;
user: TUsers;
orgId: string;
token: string;
}
| {
authMode: AuthMode.SERVICE_TOKEN;
@@ -38,6 +40,7 @@ export type TAuthMode =
serviceTokenId: string;
orgId: string;
authMethod: null;
token: string;
}
| {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN;
@@ -47,6 +50,7 @@ export type TAuthMode =
orgId: string;
authMethod: null;
isInstanceAdmin?: boolean;
token: TIdentityAccessTokenJwtPayload;
}
| {
authMode: AuthMode.SCIM_TOKEN;
@@ -56,7 +60,7 @@ export type TAuthMode =
authMethod: null;
};
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
export const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
const apiKey = req.headers?.["x-api-key"];
if (apiKey) {
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
@@ -133,7 +137,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
actor,
orgId: orgId as string,
authMethod: token.authMethod,
isMfaVerified: token.isMfaVerified
isMfaVerified: token.isMfaVerified,
token
};
break;
}
@@ -148,7 +153,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
identityId: identity.identityId,
identityName: identity.name,
authMethod: null,
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId)
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
token
};
if (token?.identityAuth?.oidc) {
requestContext.set("identityAuthInfo", {
@@ -179,7 +185,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
serviceToken,
serviceTokenId: serviceToken.id,
actor,
authMethod: null
authMethod: null,
token
};
break;
}
@@ -191,7 +198,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
actor,
user,
orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon!
authMethod: null
authMethod: null,
token: token as string
};
break;
}

View File

@@ -4,6 +4,8 @@ import { Probot } from "probot";
import { z } from "zod";
import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types";
import { TGitLabDataSourcePushEventPayload } from "@app/ee/services/secret-scanning-v2/gitlab";
import { GitLabWebHookEvent } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -113,4 +115,36 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
return res.send("ok");
}
});
// gitlab push event webhook
server.route({
method: "POST",
url: "/gitlab",
config: {
rateLimit: writeLimit
},
handler: async (req, res) => {
const event = req.headers["x-gitlab-event"] as GitLabWebHookEvent;
const token = req.headers["x-gitlab-token"] as string;
const dataSourceId = req.headers["x-data-source-id"] as string;
if (event !== GitLabWebHookEvent.Push) {
return res.status(400).send({ message: `Event type not supported: ${event as string}` });
}
if (!token) {
return res.status(401).send({ message: "Unauthorized: Missing token" });
}
if (!dataSourceId) return res.status(400).send({ message: "Data Source ID header is required" });
await server.services.secretScanningV2.gitlab.handlePushEvent({
dataSourceId,
payload: req.body as TGitLabDataSourcePushEventPayload,
token
});
return res.send("ok");
}
});
};

View File

@@ -31,6 +31,8 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { eventBusFactory } from "@app/ee/services/event/event-bus-service";
import { sseServiceFactory } from "@app/ee/services/event/event-sse-service";
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
@@ -43,6 +45,8 @@ import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
@@ -392,6 +396,7 @@ export const registerRoutes = async (
const identityProjectDAL = identityProjectDALFactory(db);
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityAuthTemplateDAL = identityAuthTemplateDALFactory(db);
const identityTokenAuthDAL = identityTokenAuthDALFactory(db);
const identityUaDAL = identityUaDALFactory(db);
@@ -495,6 +500,9 @@ export const registerRoutes = async (
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
const secretScanningV2DAL = secretScanningV2DALFactory(db);
const eventBusService = eventBusFactory(server.redis);
const sseService = sseServiceFactory(eventBusService, server.redis);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@@ -552,7 +560,8 @@ export const registerRoutes = async (
queueService,
projectDAL,
licenseService,
auditLogStreamDAL
auditLogStreamDAL,
eventBusService
});
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
@@ -766,7 +775,6 @@ export const registerRoutes = async (
orgRoleDAL,
permissionService,
orgDAL,
projectBotDAL,
incidentContactDAL,
tokenService,
projectUserAdditionalPrivilegeDAL,
@@ -841,7 +849,6 @@ export const registerRoutes = async (
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
userDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
@@ -1044,6 +1051,15 @@ export const registerRoutes = async (
kmsService
});
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const secretSyncQueue = secretSyncQueueFactory({
queueService,
secretSyncDAL,
@@ -1067,7 +1083,8 @@ export const registerRoutes = async (
secretVersionTagV2BridgeDAL,
resourceMetadataDAL,
appConnectionDAL,
licenseService
licenseService,
gatewayService
});
const secretQueueService = secretQueueFactory({
@@ -1119,11 +1136,9 @@ export const registerRoutes = async (
projectBotService,
identityProjectDAL,
identityOrgMembershipDAL,
projectKeyDAL,
userDAL,
projectEnvDAL,
orgDAL,
orgService,
projectMembershipDAL,
projectRoleDAL,
folderDAL,
@@ -1143,7 +1158,6 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL,
keyStore,
kmsService,
projectBotDAL,
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL,
@@ -1238,6 +1252,7 @@ export const registerRoutes = async (
const secretV2BridgeService = secretV2BridgeServiceFactory({
folderDAL,
projectDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
folderCommitService,
secretQueueService,
@@ -1444,6 +1459,15 @@ export const registerRoutes = async (
identityMetadataDAL
});
const identityAuthTemplateService = identityAuthTemplateServiceFactory({
identityAuthTemplateDAL,
identityLdapAuthDAL,
permissionService,
kmsService,
licenseService,
auditLogService
});
const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL,
identityOrgMembershipDAL,
@@ -1489,15 +1513,6 @@ export const registerRoutes = async (
licenseService
});
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
@@ -1596,7 +1611,8 @@ export const registerRoutes = async (
identityAccessTokenDAL,
identityOrgMembershipDAL,
licenseService,
identityDAL
identityDAL,
identityAuthTemplateDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({
@@ -1931,7 +1947,8 @@ export const registerRoutes = async (
projectMembershipDAL,
smtpService,
kmsService,
keyStore
keyStore,
appConnectionDAL
});
const secretScanningV2Service = secretScanningV2ServiceFactory({
@@ -1940,7 +1957,8 @@ export const registerRoutes = async (
licenseService,
secretScanningV2DAL,
secretScanningV2Queue,
kmsService
kmsService,
appConnectionDAL
});
// setup the communication with license key server
@@ -1964,6 +1982,7 @@ export const registerRoutes = async (
await kmsService.startService();
await microsoftTeamsService.start();
await dynamicSecretQueueService.init();
await eventBusService.init();
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
@@ -1997,6 +2016,7 @@ export const registerRoutes = async (
webhook: webhookService,
serviceToken: serviceTokenService,
identity: identityService,
identityAuthTemplate: identityAuthTemplateService,
identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService,
identityTokenAuth: identityTokenAuthService,
@@ -2070,7 +2090,9 @@ export const registerRoutes = async (
githubOrgSync: githubOrgSyncConfigService,
folderCommit: folderCommitService,
secretScanningV2: secretScanningV2Service,
reminder: reminderService
reminder: reminderService,
bus: eventBusService,
sse: sseService
});
const cronJobs: CronJob[] = [];
@@ -2131,7 +2153,8 @@ export const registerRoutes = async (
inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(),
secretScanningConfigured: z.boolean().optional(),
samlDefaultOrgSlug: z.string().optional()
samlDefaultOrgSlug: z.string().optional(),
auditLogStorageDisabled: z.boolean().optional()
})
}
},
@@ -2158,7 +2181,8 @@ export const registerRoutes = async (
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured,
secretScanningConfigured: cfg.isSecretScanningConfigured,
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug,
auditLogStorageDisabled: Boolean(cfg.DISABLE_AUDIT_LOG_STORAGE)
};
}
});
@@ -2186,5 +2210,7 @@ export const registerRoutes = async (
server.addHook("onClose", async () => {
cronJobs.forEach((job) => job.stop());
await telemetryService.flushAll();
await eventBusService.close();
sseService.close();
});
};

View File

@@ -271,7 +271,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
auditLogsRetentionDays: true,
hasDeleteProtection: true,
secretSharing: true,
showSnapshotsLegacy: true
showSnapshotsLegacy: true,
secretDetectionIgnoreValues: true
});
export const SanitizedTagSchema = SecretTagsSchema.pick({

View File

@@ -52,7 +52,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
defaultAuthOrgAuthEnforced: z.boolean().nullish(),
defaultAuthOrgAuthMethod: z.string().nullish(),
isSecretScanningDisabled: z.boolean(),
kubernetesAutoFetchServiceAccountToken: z.boolean()
kubernetesAutoFetchServiceAccountToken: z.boolean(),
paramsFolderSecretDetectionEnabled: z.boolean()
})
})
}
@@ -67,7 +68,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
fipsEnabled: crypto.isFipsModeEnabled(),
isMigrationModeOn: serverEnvs.MAINTENANCE_MODE,
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING,
kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN
kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN,
paramsFolderSecretDetectionEnabled: serverEnvs.PARAMS_FOLDER_SECRET_DETECTION_ENABLED
}
};
}
@@ -462,6 +464,42 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "DELETE",
url: "/user-management/users",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
userIds: z.string().array()
}),
response: {
200: z.object({
users: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
}).array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const users = await server.services.superAdmin.deleteUsers(req.body.userIds);
return {
users
};
}
});
server.route({
method: "PATCH",
url: "/user-management/users/:userId/admin-access",
@@ -685,6 +723,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
hide: false,
body: z.object({
email: z.string().email().trim().min(1),
password: z.string().trim().min(1),

View File

@@ -75,6 +75,10 @@ import {
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
import {
NetlifyConnectionListItemSchema,
SanitizedNetlifyConnectionSchema
} from "@app/services/app-connection/netlify";
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
import {
PostgresConnectionListItemSchema,
@@ -145,6 +149,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options,
...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedNetlifyConnectionSchema.options,
...SanitizedOktaConnectionSchema.options
]);
@@ -184,6 +189,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema,
DigitalOceanConnectionListItemSchema,
NetlifyConnectionListItemSchema,
OktaConnectionListItemSchema
]);

View File

@@ -46,7 +46,6 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi
const { connectionId } = req.params;
const projects = await server.services.appConnection.cloudflare.listPagesProjects(connectionId, req.permission);
return projects;
}
});
@@ -73,9 +72,36 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission);
const scripts = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission);
return scripts;
}
});
return projects;
server.route({
method: "GET",
url: `/:connectionId/cloudflare-zones`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const zones = await server.services.appConnection.cloudflare.listZones(connectionId, req.permission);
return zones;
}
});
};

View File

@@ -26,6 +26,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
@@ -76,5 +77,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Checkly]: registerChecklyConnectionRouter,
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter
};

View File

@@ -0,0 +1,87 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateNetlifyConnectionSchema,
SanitizedNetlifyConnectionSchema,
UpdateNetlifyConnectionSchema
} from "@app/services/app-connection/netlify";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerNetlifyConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Netlify,
server,
sanitizedResponseSchema: SanitizedNetlifyConnectionSchema,
createSchema: CreateNetlifyConnectionSchema,
updateSchema: UpdateNetlifyConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/accounts`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
accounts: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const accounts = await server.services.appConnection.netlify.listAccounts(connectionId, req.permission);
return { accounts };
}
});
server.route({
method: "GET",
url: `/:connectionId/accounts/:accountId/sites`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid(),
accountId: z.string()
}),
response: {
200: z.object({
sites: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId, accountId } = req.params;
const sites = await server.services.appConnection.netlify.listSites(connectionId, req.permission, accountId);
return { sites };
}
});
};

View File

@@ -49,4 +49,32 @@ export const registerRenderConnectionRouter = async (server: FastifyZodProvider)
return services;
}
});
server.route({
method: "GET",
url: `/:connectionId/environment-groups`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const groups = await server.services.appConnection.render.listEnvironmentGroups(connectionId, req.permission);
return groups;
}
});
};

View File

@@ -0,0 +1,125 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { subject } from "@casl/ability";
import { pipeline } from "stream/promises";
import { z } from "zod";
import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
import { EventRegisterSchema } from "@app/ee/services/event/types";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs";
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerEventRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/subscribe/project-events",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Events],
description: "Subscribe to project events",
body: z.object({
projectId: z.string().trim().describe(EventSubscriptions.SUBSCRIBE_PROJECT_EVENTS.projectId),
register: z.array(EventRegisterSchema).min(1).max(10)
}),
produces: ["text/event-stream"]
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, reply) => {
try {
const { sse, permission, identityAccessToken, authToken, license } = req.server.services;
const plan = await license.getPlan(req.auth.orgId);
if (!plan.eventSubscriptions) {
throw new BadRequestError({
message:
"Failed to use event subscriptions due to plan restriction. Upgrade plan to access enterprise event subscriptions."
});
}
const count = await sse.getActiveConnectionsCount(req.body.projectId, req.permission.id);
if (count >= 5) {
throw new RateLimitError({
message: `Too many active connections for project ${req.body.projectId}. Please close some connections before opening a new one.`
});
}
const client = await sse.subscribe({
type: ProjectType.SecretManager,
registered: req.body.register,
async getAuthInfo() {
const ability = await permission.getProjectPermission({
actor: req.auth.actor,
projectId: req.body.projectId,
actionProjectType: ActionProjectType.Any,
actorAuthMethod: req.auth.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId
});
return { permission: ability.permission, actorId: req.permission.id, projectId: req.body.projectId };
},
async onAuthRefresh(info) {
switch (req.auth.authMode) {
case AuthMode.JWT:
await authToken.fnValidateJwtIdentity(req.auth.token);
break;
case AuthMode.IDENTITY_ACCESS_TOKEN:
await identityAccessToken.fnValidateIdentityAccessToken(req.auth.token, req.realIp);
break;
default:
throw new Error("Unsupported authentication method");
}
req.body.register.forEach((r) => {
const fields = {
environment: r.conditions?.environmentSlug ?? "",
secretPath: r.conditions?.secretPath ?? "/",
eventType: r.event
};
const allowed = info.permission.can(
ProjectPermissionSecretActions.Subscribe,
subject(ProjectPermissionSub.Secrets, fields)
);
if (!allowed) {
throw new ForbiddenRequestError({
name: "PermissionDenied",
message: `You are not allowed to subscribe on secrets`,
details: {
event: fields.eventType,
environmentSlug: fields.environment,
secretPath: fields.secretPath
}
});
}
});
}
});
// Switches to manual response and enable SSE streaming
reply.hijack();
reply.raw.writeHead(200, getServerSentEventsHeaders()).flushHeaders();
reply.raw.on("close", client.abort);
await pipeline(client.stream, reply.raw, { signal: client.signal });
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
// If the stream is aborted, we don't need to do anything
return;
}
throw error;
}
}
});
};

View File

@@ -200,49 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
params: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
}),
body: z
.object({
url: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.url),
bindDN: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindDN),
bindPass: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindPass),
searchBase: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.searchBase),
searchFilter: z
.string()
.trim()
.min(1)
.default("(uid={{username}})")
.refine(isValidLdapFilter, "Invalid LDAP search filter")
.describe(LDAP_AUTH.ATTACH.searchFilter),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
body: z.union([
// Template-based configuration
z
.object({
templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId),
searchFilter: z
.string()
.trim()
.min(1)
.default("(uid={{username}})")
.refine(isValidLdapFilter, "Invalid LDAP search filter")
.describe(LDAP_AUTH.ATTACH.searchFilter),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
// Manual configuration
z
.object({
url: z.string().trim().describe(LDAP_AUTH.ATTACH.url),
bindDN: z.string().trim().describe(LDAP_AUTH.ATTACH.bindDN),
bindPass: z.string().trim().describe(LDAP_AUTH.ATTACH.bindPass),
searchBase: z.string().trim().describe(LDAP_AUTH.ATTACH.searchBase),
searchFilter: z
.string()
.trim()
.min(1)
.default("(uid={{username}})")
.refine(isValidLdapFilter, "Invalid LDAP search filter")
.describe(LDAP_AUTH.ATTACH.searchFilter),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
)
]),
response: {
200: z.object({
identityLdapAuth: IdentityLdapAuthsSchema.omit({
@@ -275,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
allowedFields: req.body.allowedFields
allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId
}
}
});
@@ -309,6 +365,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN),
bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass),
searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase),
templateId: z.string().trim().optional().describe(LDAP_AUTH.UPDATE.templateId),
searchFilter: z
.string()
.trim()
@@ -376,7 +433,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
allowedFields: req.body.allowedFields
allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId
}
}
});
@@ -413,7 +471,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
}).extend({
bindDN: z.string(),
bindPass: z.string(),
ldapCaCertificate: z.string().optional()
ldapCaCertificate: z.string().optional(),
templateId: z.string().optional().nullable()
})
})
}

View File

@@ -13,6 +13,7 @@ import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerEventRouter } from "./event-router";
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router";
@@ -183,4 +184,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
},
{ prefix: "/reminders" }
);
await server.register(registerEventRouter, { prefix: "/events" });
};

View File

@@ -247,7 +247,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true,
superAdmin: true
}).merge(z.object({ publicKey: z.string().nullable() }))
}).merge(z.object({ publicKey: z.string().nullable().optional() }))
})
)
.omit({ createdAt: true, updatedAt: true })

View File

@@ -9,73 +9,6 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/srp1",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
clientPublicKey: z.string().trim()
}),
response: {
200: z.object({
serverPublicKey: z.string(),
salt: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { salt, serverPublicKey } = await server.services.password.generateServerPubKey(
req.permission.id,
req.body.clientPublicKey
);
return { salt, serverPublicKey };
}
});
server.route({
method: "POST",
url: "/change-password",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
clientProof: z.string().trim(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => {
const appCfg = getConfig();
await server.services.password.changePassword({ ...req.body, userId: req.permission.id });
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully changed password" };
}
});
server.route({
method: "POST",
url: "/email/password-reset",
@@ -131,41 +64,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/backup-private-key",
config: {
rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
clientProof: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
iv: z.string().trim(),
tag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
backupPrivateKey: BackupPrivateKeySchema.omit({ verifier: true })
})
}
},
handler: async (req) => {
const token = validateSignUpAuthorization(req.headers.authorization as string, "", false)!;
const backupPrivateKey = await server.services.password.createBackupPrivateKey({
...req.body,
userId: token.userId
});
if (!backupPrivateKey) throw new Error("Failed to create backup key");
return { message: "Successfully updated backup private key", backupPrivateKey };
}
});
server.route({
method: "GET",
url: "/backup-private-key",
@@ -257,14 +155,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
},
schema: {
body: z.object({
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim(),
token: z.string().trim()
}),

View File

@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({
publicKeys: z
.object({
publicKey: z.string().optional(),
publicKey: z.string().nullable().optional(),
userId: z.string()
})
.array()
@@ -369,7 +369,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.describe(PROJECTS.UPDATE.slug),
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing),
showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy),
defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct)
defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct),
secretDetectionIgnoreValues: z
.array(z.string())
.optional()
.describe(PROJECTS.UPDATE.secretDetectionIgnoreValues)
}),
response: {
200: z.object({
@@ -392,7 +396,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
hasDeleteProtection: req.body.hasDeleteProtection,
slug: req.body.slug,
secretSharing: req.body.secretSharing,
showSnapshotsLegacy: req.body.showSnapshotsLegacy
showSnapshotsLegacy: req.body.showSnapshotsLegacy,
secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues
},
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,

View File

@@ -22,6 +22,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
message: z.string().trim().max(1024).optional(),
repeatDays: z.number().min(1).nullable().optional(),
nextReminderDate: z.string().datetime().nullable().optional(),
fromDate: z.string().datetime().nullable().optional(),
recipients: z.string().array().optional()
})
.refine((data) => {
@@ -45,6 +46,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
message: req.body.message,
repeatDays: req.body.repeatDays,
nextReminderDate: req.body.nextReminderDate,
fromDate: req.body.fromDate,
recipients: req.body.recipients
}
});

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