Compare commits

...

521 Commits

Author SHA1 Message Date
0191eb48f3 Merge pull request #3974 from Infisical/fix-email-invite-notifications
Improve + fix invitation reminder logic
2025-07-07 14:47:50 -04:00
9d39910152 Minor fix to prevent setting lastInvitedAt for invitees who weren’t actually sent an invitation 2025-07-07 15:35:49 -03:00
9137fa4ca5 Improve + fix invitation reminder logic 2025-07-07 13:31:20 -04:00
78da7ec343 Merge pull request #3972 from Infisical/fix/telemetryOrgIdentify
feat(telemetry): improve Posthog org identity logic
2025-07-07 10:15:59 -03:00
a678ebb4ac Fix Cloud telemetry queue initialization 2025-07-07 10:10:30 -03:00
83dd38db49 feat(telemetry): reduce TELEMETRY_AGGREGATED_KEY_EXP to 10 mins and avoid sending org identitfy events for batch events on sendPostHogEvents 2025-07-07 08:36:15 -03:00
06f5af1200 Merge pull request #3890 from Infisical/daniel/sso-endpoints-docs
docs(api-reference/organizations): document SSO configuration endpoints
2025-07-04 05:33:52 +04:00
f903e5b3d4 Update saml-router.ts 2025-07-04 05:23:05 +04:00
c6f8915d3f Update saml-config-service.ts 2025-07-04 05:21:54 +04:00
65b1354ef1 fix: remove undefined return type from get saml endpoint 2025-07-04 05:07:54 +04:00
cda8579ca4 fix: requested changes 2025-07-04 04:51:14 +04:00
1b1acdcb0b Merge pull request #3917 from Infisical/cli-add-bitbucket-platform
Add BitBucket platform to secret scanning
2025-07-03 20:06:48 -04:00
a8f08730a1 Merge pull request #3908 from Infisical/fix/ui-small-catches
feat: added autoplay to loading lottie and fixed tooltip in project select
2025-07-03 19:35:59 -04:00
9af9050aa2 Merge pull request #3921 from Infisical/misc/allow-users-with-create-identity-to-invite-no-access
misc: allow users with create permission to add identities with no access
2025-07-03 19:27:04 -04:00
cc564119e0 misc: allow users with create permission to add identities with no access 2025-07-04 04:24:15 +08:00
189b0dd5ee Merge pull request #3920 from Infisical/fix-secret-sync-remove-and-import-audit-logs
fix(secret-syncs): pass audit log info from import/delete secrets for sync endpoint
2025-07-03 13:02:04 -07:00
9cbef2c07b fix: pass audit log info from import/delete secrets for sync endpoint 2025-07-03 12:37:28 -07:00
9a960a85cd Merge pull request #3905 from Infisical/password-reset-ui
improvement(password-reset): re-vamp password reset flow pages/steps to match login
2025-07-03 10:31:58 -07:00
2a9e31d305 Few nits 2025-07-03 13:11:53 -04:00
fb2f1731dd Merge branch 'main' into password-reset-ui 2025-07-03 13:02:48 -04:00
42648a134c Update utils.go to look more like Gitleaks version 2025-07-03 12:47:25 -04:00
defb66ce65 Merge pull request #3918 from Infisical/revert-3901-revert-3875-ENG-3009-test
Undo Environment Variables Override PR Revert + SSO Fix
2025-07-03 12:18:10 -04:00
a3d06fdf1b misc: added reference to server admin 2025-07-03 21:21:06 +08:00
9049c441d6 Greptile review fix 2025-07-03 03:18:37 -04:00
51ecc9dfa0 Merge branch 'revert-3899-revert-3896-misc/final-changes-for-self-serve-en' into revert-3901-revert-3875-ENG-3009-test 2025-07-03 03:08:42 -04:00
13c9879fb6 Merge branch 'main' into revert-3901-revert-3875-ENG-3009-test 2025-07-03 02:54:28 -04:00
23b20ebdab Fix CLI always defaulting to github 2025-07-03 00:49:31 -04:00
37d490ede3 Add BitBucket platform to secret scanning 2025-07-03 00:09:28 -04:00
73025f5094 Merge pull request #3916 from Infisical/revert-3915-revert-3914-daniel/infisical-helm
Revert "Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support""
2025-07-03 05:25:24 +04:00
82634983ce Update Chart.yaml 2025-07-03 05:19:30 +04:00
af2f3017b7 fix: tests failing 2025-07-03 05:13:50 +04:00
a8f0eceeb9 Update helm-release-infisical-core.yml 2025-07-03 05:00:51 +04:00
36ff5e054b Update helm-release-infisical-core.yml 2025-07-03 04:50:49 +04:00
eff73f1810 fix: update versions 2025-07-03 04:27:55 +04:00
68357b5669 Revert "Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support"" 2025-07-02 20:25:36 -04:00
03c2e93bea Merge pull request #3915 from Infisical/revert-3914-daniel/infisical-helm
Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support"
2025-07-02 20:25:33 -04:00
8c1f3837e7 Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support" 2025-07-03 04:24:40 +04:00
7b47d91cc1 Merge pull request #3914 from Infisical/daniel/infisical-helm
feat(helm-charts/infiscal-core): topologySpreadConstraints support
2025-07-03 04:21:34 +04:00
c37afaa050 feat(helm-charts/infiscal-core): topologySpreadConstraints support 2025-07-03 04:08:37 +04:00
811920f8bb Merge pull request #3870 from Infisical/feat/zabbixSyncIntegration
feat(secret-sync): add Zabbix secret sync
2025-07-02 20:59:51 -03:00
7b295c5a21 Merge pull request #3913 from Infisical/daniel/fix-folder-deletion
fix(secret-folders): delete folder by ID
2025-07-03 03:49:01 +04:00
527a727c1c fix: ts issue 2025-07-03 03:28:21 +04:00
0139064aaa Update secret-folder-service.ts 2025-07-03 03:17:10 +04:00
a3859170fe fix(secret-folders): delete folder by ID 2025-07-03 03:15:06 +04:00
02b97cbf5b Merge pull request #3912 from Infisical/fix/multiEnvDeleteErrorMessage
Improve multi-env error message to show full env name instead of slug
2025-07-02 17:43:32 -04:00
8a65343f79 Add 15 seconds default duration for toast notifications 2025-07-02 18:42:02 -03:00
cf6181eb73 Improve multi-env error message to show full env name instead of slug 2025-07-02 18:25:49 -03:00
984ffd2a53 Merge pull request #3911 from Infisical/fix/policyFolderDeletionAndBatchMessage
Fix root folder issue with folder policies check and multi env error message improvement
2025-07-02 17:46:18 -03:00
a1c44bd7a2 Improve multi-env error message 2025-07-02 17:40:37 -03:00
d7860e2491 Merge pull request #3904 from Infisical/secret-overview-expandable-header
improvement: allow users to expand collapsed environment view header
2025-07-02 12:51:02 -07:00
db33349f49 Merge pull request #3910 from Infisical/misc/updated-worker-count-for-secret-scanning-jobs
misc: downsize worker count for secret scanning jobs
2025-07-02 12:50:37 -07:00
=
7ab67db84d feat: fixed black color in tooltip 2025-07-03 01:18:52 +05:30
e14bb6b901 Fix root folder issue with folder policies check and multi env error message improvement 2025-07-02 16:22:16 -03:00
=
3a17281e37 feat: resolved tooltip overflow 2025-07-03 00:41:47 +05:30
91d6d5d07b misc: updated worker count for secret scanning jobs 2025-07-03 03:02:16 +08:00
ac7b23da45 Merge pull request #3909 from Infisical/misc/update-tooltip-for-overwrite-sync
misc: update tooltip for overwrite sync
2025-07-03 02:57:52 +08:00
1fdc82e494 misc: update tooltip for overwrite sync 2025-07-03 02:32:10 +08:00
3daae6f965 improvement: adjust header drag to use table container for positioning 2025-07-02 11:10:37 -07:00
833963af0c improvement: remove additional relative and adjust handle position 2025-07-02 11:01:51 -07:00
aa560b8199 improvement: address feedback 2025-07-02 10:57:14 -07:00
a215b99b3c Merge pull request #3906 from Infisical/feat/audit-log-fix
feat: audit log improvement
2025-07-03 01:49:06 +08:00
=
fbd9ecd980 feat: fixed ts error 2025-07-02 23:04:36 +05:30
=
3b839d4826 feat: addressed review comments 2025-07-02 23:04:36 +05:30
=
b52ec37f76 feat: added query size validation for audit log 2025-07-02 23:04:36 +05:30
=
5709afe0d3 feat: lint errors fix 2025-07-02 23:04:36 +05:30
=
566a243520 feat: seperated date filter 2025-07-02 23:04:36 +05:30
=
147c21ab9f feat: updated backend logic to use parition and speed up audit log queries 2025-07-02 23:04:36 +05:30
=
abfe185a5b feat: added autoplay to loading lottie and fixed tooltip in project select 2025-07-02 22:13:37 +05:30
f62eb9f8a2 Merge pull request #3892 from Infisical/ENG-1946
feat: Re-invite users every 1 week for up to a month.
2025-07-02 12:08:13 -04:00
ec60080e27 Merge pull request #3907 from Infisical/misc/update-cli-releaser-spec
misc: updated CLI releaser spec
2025-07-02 10:44:55 -04:00
9fdc56bd6c misc: updated CLI releaser spec 2025-07-02 22:41:51 +08:00
9163da291e feat(secret-sync): add PR suggestions for Zabbix secret sync 2025-07-02 10:18:20 -03:00
f6c10683a5 misc: add sync for passport middleware 2025-07-02 20:48:24 +08:00
307e6900ee Merge branch 'main' into feat/zabbixSyncIntegration 2025-07-02 09:25:19 -03:00
bb59bb1868 Remove file 2025-07-01 22:46:16 -04:00
139f880be1 merge 2025-07-01 22:43:20 -04:00
69157cb912 improvement: add period 2025-07-01 19:23:13 -07:00
44eb761d5b improvement: re-vamp password reset flow pages/steps to match login design 2025-07-01 19:19:27 -07:00
f6002d81b3 Merge pull request #3872 from Infisical/feat/team-autonomy-product-migration
feat: project ui v3
2025-07-01 21:09:43 -04:00
af240bd58c Merge pull request #3886 from Infisical/policy-delete-requests-warning
improvement(approval-policies): Add open request warning to remove policy modal
2025-07-01 18:07:22 -07:00
414de3c4d0 update broken import 2025-07-01 20:26:19 -04:00
1a7b810bad improvement: allow users to expand collapsed environment view header 2025-07-01 17:22:49 -07:00
0379ba4eb1 Merge branch 'main' into feat/team-autonomy-product-migration 2025-07-01 20:21:00 -04:00
c2ce1aa5aa Fix license fns 2025-07-01 20:06:51 -04:00
c8e155f0ca Review fixes 2025-07-01 19:48:17 -04:00
5ced43574d Merge pull request #3903 from Infisical/fix/blockFolderDeletionOnPolicyInPlace
feat(change-approvals): block folder deletion if there is at least one secret protected by a policy
2025-07-01 20:39:28 -03:00
19ff045d2e improvement: address feedback 2025-07-01 16:13:14 -07:00
4784f47a72 Merge pull request #3898 from Infisical/daniel/remove-mint
docs: remove mint.json file in favor of docs.json
2025-07-01 19:01:42 -04:00
abbf541c9f Docs link on UI 2025-07-01 19:01:39 -04:00
28a27daf29 feat(change-approvals): block folder deletion if there is at least one secret protected by a policy 2025-07-01 19:55:38 -03:00
fcdd121a58 Docs & UI update 2025-07-01 18:46:06 -04:00
5bfd92bf8d Revert "Revert "feat(super-admin): Environment Overrides"" 2025-07-01 17:43:52 -04:00
83f0a500bd Merge pull request #3901 from Infisical/revert-3875-ENG-3009
Revert "feat(super-admin): Environment Overrides"
2025-07-01 17:43:49 -04:00
325d277021 Revert "feat(super-admin): Environment Overrides" 2025-07-01 17:43:38 -04:00
45af2c0b49 Revert "Revert "misc: updated sidebar name"" 2025-07-01 17:42:54 -04:00
9ca71f663a Merge pull request #3899 from Infisical/revert-3896-misc/final-changes-for-self-serve-en
Revert "misc: updated sidebar name"
2025-07-01 17:42:51 -04:00
e5c7aba745 Revert "misc: updated sidebar name" 2025-07-01 17:42:33 -04:00
cada75bd0c Delete mint.json 2025-07-02 01:29:49 +04:00
a37689eeca Merge pull request #3897 from Infisical/misc/add-plain-support-for-user-get-token-cli
misc: add plain support for user get token in CLI
2025-07-01 17:04:45 -04:00
38c9242e5b misc: add plain support for user get token in CLI 2025-07-02 04:45:53 +08:00
8dafa75aa2 Merge pull request #3896 from Infisical/misc/final-changes-for-self-serve-en
misc: updated sidebar name
2025-07-01 16:28:05 -04:00
aea61bae38 misc: label updates 2025-07-02 04:17:52 +08:00
37a10d1435 misc: updated sidebar name 2025-07-02 04:13:58 +08:00
=
a64c2173e7 feat: resolved broken row 2025-07-02 01:33:02 +05:30
=
ec0603a464 feat: resolved merge reviews 2025-07-02 01:16:52 +05:30
=
bf8d60fcdc feat: resolved merge issues 2025-07-02 01:16:52 +05:30
=
b47846a780 feat: resolved type filter in ssh project 2025-07-02 01:16:52 +05:30
=
ea403b0393 feat: resolved review comments 2025-07-02 01:16:52 +05:30
=
9ab89fdef6 feat: resolved all broken urls in backend redirect 2025-07-02 01:16:52 +05:30
=
dea22ab844 feat: removed all getProjectFromSplitId 2025-07-02 01:16:52 +05:30
=
8bdf294a34 feat: added default product switch in project settings 2025-07-02 01:16:51 +05:30
=
0b2c967e63 feat: renamed defaultType to defaultProduct 2025-07-02 01:16:51 +05:30
=
c89876aa10 feat: corrected title for layout 2025-07-02 01:16:51 +05:30
=
76b3aab4c0 feat: removed hover thing 2025-07-02 01:16:51 +05:30
=
944319b9b6 feat: resolved alignement issue 2025-07-02 01:16:51 +05:30
ac6f79815a fix ui for navbar 2025-07-02 01:16:51 +05:30
=
6734bf245f feat: corrected icon again and fixed incorrect title in settings page of products 2025-07-02 01:16:50 +05:30
=
b32584ce73 feat: changed vault lottie 2025-07-02 01:16:50 +05:30
=
3e41b359c5 feat: changed layout to absolute 2025-07-02 01:16:50 +05:30
=
2352bca03e feat: resolved sidebar alignment issue of server admin 2025-07-02 01:16:50 +05:30
=
9f3236b47d feat: added search to project nav 2025-07-02 01:16:50 +05:30
=
01c5f516f8 feat: resolved license-fn type error 2025-07-02 01:16:50 +05:30
=
74067751a6 feat: updated lotties for the products 2025-07-02 01:16:50 +05:30
=
fa7318eeb1 feat: done and dusted - new plasma ui 2025-07-02 01:16:49 +05:30
=
fb9c580e53 feat: fixed padding in layout 2025-07-02 01:16:49 +05:30
=
1bfdbb7314 feat: removed filters made in project roles 2025-07-02 01:16:49 +05:30
=
6b3279cbe5 feat: completed breadcrumb and settings changes 2025-07-02 01:16:49 +05:30
=
48ac6b4aff feat: fixed all ts url errors 2025-07-02 01:16:49 +05:30
=
b0c1c9ce26 feat: added project settings and access management 2025-07-02 01:16:48 +05:30
=
d82d22a198 feat: seperated layouts for each product line 2025-07-02 01:16:48 +05:30
=
c66510f473 feat: completed the product sidebar 2025-07-02 01:16:48 +05:30
=
09cdd5ec91 feat: added project layout and project select in breadcrumb 2025-07-02 01:16:48 +05:30
=
e028b4e26d feat: removed all action project type check 2025-07-02 01:16:48 +05:30
=
b8f7ffbf53 feat: re-arranged org project pages 2025-07-02 01:16:47 +05:30
=
0d97fc27c7 feat: moved org breadcrumbs to top level 2025-07-02 01:16:47 +05:30
=
098c1d840b feat: org sidebar first version 2025-07-02 01:16:47 +05:30
cce2a54265 Merge pull request #3883 from Infisical/doc/add-mention-of-default-audience-support
doc: add mention of default audience support for CSI
2025-07-01 14:35:15 -04:00
d1033cb324 Merge pull request #3875 from Infisical/ENG-3009
feat(super-admin): Environment Overrides
2025-07-02 02:18:40 +08:00
7134e1dc66 misc: updated success notif 2025-07-02 02:18:04 +08:00
8aa26b77ed Fix check 2025-07-01 13:11:15 -04:00
4b06880320 Feedback fixes 2025-07-01 11:52:01 -04:00
124cd9f812 Merge pull request #3893 from Infisical/misc/added-missing-project-cert-endpoints-to-open-api-spec
misc: added missing project cert endpoints to open api spec
2025-07-01 23:39:37 +08:00
d531d069d1 Add azure app connection 2025-07-01 11:23:44 -04:00
522a5d477d Merge pull request #3889 from Infisical/minor-access-approval-modal-improvements
improvement(approval-policy): minor create policy layout adjustments
2025-07-01 08:21:26 -07:00
d2f0db669a Merge pull request #3894 from Infisical/fix/address-instance-of-github-dynamic-secret
fix: address instanceof check in github dynamic secret
2025-07-01 23:11:01 +08:00
4dd78d745b fix: address instanceof check in github dynamic secret 2025-07-01 20:45:00 +08:00
4fef5c305d misc: added missing project cert endpoints to open api spec 2025-07-01 18:53:13 +08:00
e5bbc46b0f Add org caching + fix a line 2025-07-01 00:07:10 -04:00
30f3543850 Merge pull request #3876 from Infisical/ENG-2977
feat(secret-sync): Allow custom field label on 1pass sync
2025-06-30 23:36:22 -04:00
114915f913 Merge pull request #3891 from Infisical/change-request-page-improvements
improvement(secret-approval-request): Color/layout styling adjustments to change request page
2025-06-30 19:35:40 -07:00
b5801af9a8 improvements: address feedback 2025-06-30 18:32:36 -07:00
20366a8c07 improvement: address feedback 2025-06-30 18:09:50 -07:00
60a4c72a5d feat: Re-invite users every 1 week for up to a month. 2025-06-30 20:10:30 -04:00
447e28511c improvement: update stale/conflict text 2025-06-30 16:44:29 -07:00
650ed656e3 improvement: color/layout styling adjustments to change request page 2025-06-30 16:30:37 -07:00
13d2cbd8b0 Update docs.json 2025-07-01 02:09:14 +04:00
abfc5736fd docs(api-reference/organizations): document SSO configuration endpoints 2025-07-01 02:05:53 +04:00
54ac450b63 improvement: minor layout adjustments 2025-06-30 14:38:23 -07:00
3871fa552c Merge pull request #3888 from Infisical/revert-3885-misc/add-indices-for-referencing-columns-in-identity-access-token
Revert "misc: add indices for referencing columns in identity access token"
2025-06-30 17:27:31 -04:00
9c72ee7f10 Revert "misc: add indices for referencing columns in identity access token" 2025-07-01 05:23:51 +08:00
22e8617661 Merge pull request #3885 from Infisical/misc/add-indices-for-referencing-columns-in-identity-access-token
misc: add indices for referencing columns in identity access token
2025-06-30 17:01:20 -04:00
2f29a513cc misc: make index creation concurrently 2025-07-01 03:36:55 +08:00
cb6c28ac26 UI updates 2025-06-30 14:08:27 -04:00
d3833c33b3 Merge pull request #3878 from Infisical/fix-approval-policy-bypassing
Fix bypassing approval policies
2025-06-30 13:37:28 -04:00
978a3e5828 misc: add indices for referencing columns in identity access token 2025-07-01 01:25:11 +08:00
27bf91e58f Merge pull request #3873 from Infisical/org-access-control-improvements
improvement(org-access-control): Standardize and improve org access control UI
2025-06-30 09:54:42 -07:00
f2c3c76c60 improvement: address feedback on remove rule policy edit 2025-06-30 09:21:00 -07:00
85023916e4 improvement: address feedback 2025-06-30 09:12:47 -07:00
3723afe595 Merge branch 'main' into ENG-3009 2025-06-30 12:01:14 -04:00
02afd6a8e7 Merge pull request #3882 from Infisical/feat/fix-access-token-ips
feat: resolved inefficient join for ip restriction in access token
2025-06-30 21:22:28 +05:30
14d6f6c048 doc: add mention of default audience support for CSI 2025-06-30 23:51:50 +08:00
=
929eac4350 feat: resolved inefficient join for ip restriction in access token 2025-06-30 20:13:26 +05:30
c6074dd69a Merge pull request #3881 from Infisical/docs-update
update spend policy
2025-06-29 18:10:54 -07:00
a9b26755ba update spend policy 2025-06-29 17:43:05 -07:00
033e5d3f81 Merge pull request #3880 from Infisical/docs-update
update logos in docs
2025-06-28 16:38:05 -07:00
90634e1913 update logos in docs 2025-06-28 16:26:58 -07:00
58b61a861a Fix bypassing approval policies 2025-06-28 04:17:09 -04:00
3c8ec7d7fb Merge pull request #3869 from Infisical/sequence-approval-policy-ui-additions
improvement(access-policies): Revamp approval sequence table display and access request modal
2025-06-28 04:07:41 -04:00
26a59286c5 Merge pull request #3877 from Infisical/remove-datadog-logs
Remove debug logs for DataDog stream
2025-06-28 03:45:14 -04:00
392792bb1e Remove debug logs for DataDog stream 2025-06-28 03:37:32 -04:00
d79a6b8f25 Lint fixes 2025-06-28 03:35:52 -04:00
217a09c97b Docs 2025-06-28 03:14:45 -04:00
a389ede03d Review fixes 2025-06-28 03:01:34 -04:00
10939fecc0 feat(super-admin): Environment Overrides 2025-06-28 02:35:38 -04:00
48f40ff938 improvement: address feedback 2025-06-27 21:00:48 -07:00
969896e431 Merge pull request #3874 from Infisical/remove-certauth-join
Remove cert auth left join
2025-06-27 20:41:58 -04:00
fd85da5739 set trusted ip to empty 2025-06-27 20:36:32 -04:00
2caf6ff94b remove cert auth left join 2025-06-27 20:21:28 -04:00
ed7d709a70 improvement: standardize and improve org access control 2025-06-27 15:15:12 -07:00
aff97374a9 Merge pull request #3868 from Infisical/misc/add-mention-of-service-usage-api-for-gcp
misc: add mention of service usage API for GCP
2025-06-28 04:26:21 +08:00
e8e90585ca Merge pull request #3871 from Infisical/project-role-type-col
improvement(project-roles): Add type col to project roles table and default sort
2025-06-27 11:42:47 -07:00
abd9dbf714 improvement: add type col to project roles table and default sort 2025-06-27 11:34:54 -07:00
89aed3640b Merge pull request #3852 from akhilmhdh/feat/tls-identity-auth
feat: TLS cert identity auth
2025-06-28 02:29:25 +08:00
5513ff7631 Merge pull request #3866 from Infisical/feat/posthogEventBatch
feat(telemetry): Add aggregated events and groups to posthog
2025-06-27 14:42:55 -03:00
9fb7676739 misc: reordered doc for mi auth 2025-06-28 01:35:46 +08:00
6ac734d6c4 removed unnecessary changes 2025-06-28 01:32:53 +08:00
8044999785 feat(telemetry): increase even redis key exp to 15 mins 2025-06-27 14:31:54 -03:00
be51e4372d feat(telemetry): addressed PR suggestions 2025-06-27 14:30:31 -03:00
460b545925 Merge branch 'feat/tls-identity-auth' of https://github.com/akhilmhdh/infisical into HEAD 2025-06-28 01:29:49 +08:00
2f26c1930b misc: doc updates 2025-06-28 01:26:24 +08:00
68abd0f044 feat(secret-sync): fix docs 2025-06-27 14:23:39 -03:00
f3c11a0a17 feat(secret-sync): fix docs 2025-06-27 14:12:46 -03:00
f4779de051 feat(secret-sync): add re2 on replacements 2025-06-27 14:03:59 -03:00
defe7b8f0b feat(secret-sync): add blockLocalAndPrivateIpAddresses on secret-sync fns functions 2025-06-27 13:37:57 -03:00
cf3113ac89 feat(secret-sync): add Zabbix secret sync 2025-06-27 13:31:41 -03:00
953cc3a850 improvements: revise approval sequence table display and access request modal 2025-06-27 09:30:11 -07:00
fc9ae05f89 misc: updated TLS acronym 2025-06-28 00:21:08 +08:00
de22a3c56b misc: updated casing of acronym 2025-06-28 00:17:42 +08:00
7c4baa6fd4 misc: added image for service usage API 2025-06-27 13:19:14 +00:00
f285648c95 misc: add mention of service usage API for GCP 2025-06-27 21:10:02 +08:00
0f04890d8f feat(telemetry): addressed PR suggestions 2025-06-26 21:18:07 -03:00
61274243e2 feat(telemetry): add batch events and groups logic 2025-06-26 20:58:01 -03:00
9366428091 Merge pull request #3865 from Infisical/remove-manual-styled-css-on-checkboxes
fix(checkbox): Remove manual css overrides of checkbox checked state
2025-06-26 15:38:05 -07:00
62482852aa fix: remove manual css overrides of checkbox checked state 2025-06-26 15:33:27 -07:00
cc02c00b61 Merge pull request #3864 from Infisical/update-aws-param-store-docs
Clarify relationship between path and key schema for AWS parameter store
2025-06-26 18:19:06 -04:00
2e256e4282 Tooltip 2025-06-26 18:14:48 -04:00
1b4bae6a84 Merge pull request #3863 from Infisical/remove-secret-scanning-v1-backend
chore(secret-scanning-v1): remove secret scanning v1 queue and webhook endpoint
2025-06-26 14:51:23 -07:00
1f0bcae0fc Merge pull request #3860 from Infisical/secret-sync-selection-improvements
improvement(secret-sync/app-connection): Add search/pagination to secret sync and app connection selection modals
2025-06-26 14:50:44 -07:00
dcd21883d1 Clarify relationship between path and key schema for AWS parameter store
docs
2025-06-26 17:02:21 -04:00
9af5a66bab feat(secret-sync): Allow custom field label on 1pass sync 2025-06-26 16:07:08 -04:00
d7913a75c2 chore: remove secret scanning v1 queue and webhook endpoint 2025-06-26 11:32:45 -07:00
205442bff5 Merge pull request #3859 from Infisical/overview-ui-improvements
improvement(secret-overview): Add collapsed environment view to secret overview page
2025-06-26 09:24:33 -07:00
8ab51aba12 improvement: add search/pagination app connection select 2025-06-26 09:21:35 -07:00
e8d19eb823 improvement: disable tooltip hover content for env name tooltip 2025-06-26 09:12:11 -07:00
3d1f054b87 improvement: add pagination/search to secret sync selection 2025-06-26 08:13:57 -07:00
5d30215ea7 improvement: increase env tooltip max width and adjust alignment 2025-06-26 07:56:47 -07:00
29fedfdde5 Merge pull request #3850 from Infisical/policy-edit-revisions
improvement(project-policies): Revamp edit role page and access tree
2025-06-26 07:46:35 -07:00
b5317d1d75 fix: add ability to remove non-conditional rules 2025-06-26 07:37:30 -07:00
86c145301e improvement: add collapsed environment view to secret overview page and minor ui adjustments 2025-06-25 16:49:34 -07:00
6446311b6d Merge pull request #3835 from Infisical/feat/gitlabSecretSync
feat(secret-sync): Add gitlab secret sync
2025-06-25 17:53:12 -03:00
3e80f1907c Merge pull request #3857 from Infisical/daniel/fix-dotnet-docs
docs: fix redirect for .NET SDK
2025-06-25 23:18:14 +04:00
79e62eec25 docs: fix redirect for .NET SDK 2025-06-25 23:11:11 +04:00
c41730c5fb Merge pull request #3856 from Infisical/daniel/fix-docs
fix(docs): sdk and changelog tab not loading
2025-06-25 22:34:09 +04:00
aac63d3097 fix(docs): sdk and changelog tab not working 2025-06-25 22:32:08 +04:00
f0b9d3c816 feat(secret-sync): improve hide secrets tooltip message 2025-06-25 14:10:28 -03:00
ea393d144a feat(secret-sync): minor change on docs 2025-06-25 13:57:07 -03:00
c4c0f86598 feat(secret-sync): improve update logic and add warning on docs for gitlab limitation on hidden variables 2025-06-25 13:51:38 -03:00
1f7617d132 Merge pull request #3851 from Infisical/ENG-3013
Allow undefined value for tags to prevent unwanted overrides
2025-06-25 12:45:43 -04:00
c95680b95d feat(secret-sync): type fix 2025-06-25 13:33:43 -03:00
18f1f93b5f Review fixes 2025-06-25 12:29:23 -04:00
70ea761375 feat(secret-sync): fix update masked_and_hidden field to not be sent unless it's true 2025-06-25 13:17:41 -03:00
5b4790ee78 improvements: truncate environment selection and only show visualize access when expanded 2025-06-25 09:09:08 -07:00
5ab2a6bb5d Feedback 2025-06-25 11:56:11 -04:00
dcac85fe6c Merge pull request #3847 from Infisical/share-your-own-secret-link-fix
fix(secret-sharing): Support self-hosted for "share your own secret" link
2025-06-25 08:31:13 -07:00
2f07471404 Merge pull request #3853 from akhilmhdh/feat/copy-token
feat: added copy token button
2025-06-25 10:55:07 -04:00
137fd5ef07 added minor text updates 2025-06-25 10:50:16 -04:00
=
883c7835a1 feat: added copy token button 2025-06-25 15:28:58 +05:30
=
e33f34ceb4 fix: corrected the doc key 2025-06-25 14:46:13 +05:30
=
af5805a5ca feat: resolved incorrect invalidation 2025-06-25 14:46:13 +05:30
bcf1c49a1b Update docs/documentation/platform/identities/tls-cert-auth.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-25 14:45:14 +05:30
84fedf8eda Update docs/documentation/platform/identities/tls-cert-auth.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-25 14:44:45 +05:30
97755981eb Update docs/documentation/platform/identities/tls-cert-auth.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-25 14:43:01 +05:30
8291663802 Update frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-25 14:42:24 +05:30
d9aed45504 Update frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-25 14:42:11 +05:30
=
8ada11edf3 feat: docs for tls cert auth 2025-06-25 14:27:04 +05:30
=
4bd62aa462 feat: updated frontend to have the tls cert auth login 2025-06-25 14:26:55 +05:30
0366e58a5b Type fix 2025-06-25 00:24:24 -03:00
9f6dca23db Greptile reviews 2025-06-24 23:19:42 -04:00
18e733c71f feat(secret-sync): minor fixes 2025-06-25 00:16:44 -03:00
f0a95808e7 Allow undefined value for tags to prevent unwanted overrides 2025-06-24 23:13:53 -04:00
90a0d0f744 Merge pull request #3848 from Infisical/improve-audit-log-streams
improve audit log streams: add backend logs + DD source
2025-06-24 22:18:04 -04:00
7f9c9be2c8 review fix 2025-06-24 22:00:45 -04:00
070982081c Merge remote-tracking branch 'origin/main' into feat/gitlabSecretSync 2025-06-24 22:42:28 -03:00
f462c3f85d feat(secret-sync): minor fixes 2025-06-24 21:38:33 -03:00
8683693103 improvement: address greptile feedback 2025-06-24 15:35:42 -07:00
737fffcceb improvement: address greptile feedback 2025-06-24 15:35:08 -07:00
ffac24ce75 improvement: revise edit role page and access tree 2025-06-24 15:23:27 -07:00
c505c5877f feat(secret-sync): updated docs 2025-06-24 18:11:18 -03:00
b59fa14bb6 Merge pull request #3818 from Infisical/feat/cli-bootstrap-create-k8-secret
feat: added auto-bootstrap support to helm
2025-06-24 17:03:13 -04:00
d4bf8a33dc feat(secret-sync): rework GitLab secret-sync to add group variables 2025-06-24 18:01:32 -03:00
0eb36d7e35 misc: final doc changes 2025-06-24 20:56:06 +00:00
ae2da0066a misc: add helm chart auto bootstrap to methods 2025-06-25 04:40:07 +08:00
6566393e21 Review fixes 2025-06-24 14:39:46 -04:00
1d7da56b40 misc: used kubernetes client 2025-06-25 02:38:51 +08:00
af245b1f16 Add "service: audit-logs" entry for DataDog 2025-06-24 14:22:26 -04:00
3d2465ae41 Merge pull request #3825 from Infisical/feat/add-cloudflare-app-connection-and-sync
feat: added cloudflare app connection and secret sync
2025-06-25 00:44:58 +08:00
c17df7e951 Improve URL detection 2025-06-24 12:44:16 -04:00
4d4953e95a improve audit log streams: add backend logs + DD source 2025-06-24 12:35:49 -04:00
f4f34802bc Merge pull request #3816 from Infisical/fix/addProjectSlugToSecretsV3
Add projectSlug parameter on secrets v3 endpoints
2025-06-24 13:28:23 -03:00
59cc857aef fix: further improve inconsistencies 2025-06-24 19:37:32 +04:00
a6713b2f76 Merge pull request #3846 from Infisical/daniel/multiple-folders
fix(folders): duplicate folders
2025-06-24 19:04:26 +04:00
3c9a7c77ff chore: re-add comment 2025-06-24 18:58:03 +04:00
f1bfea61d0 fix: replace keystore lock with postgres lock 2025-06-24 18:54:18 +04:00
144ad2f25f misc: added image for generated token 2025-06-24 14:51:11 +00:00
43e0d400f9 feat(secret-sync): add Gitlab PR comments suggestions 2025-06-24 10:05:46 -03:00
=
b80b77ec36 feat: completed backend changes for tls auth 2025-06-24 16:46:46 +05:30
02a2309953 misc: added note for bootstrap output flag 2025-06-24 18:26:17 +08:00
f1587d8375 misc: addressed comments 2025-06-24 18:18:07 +08:00
42aaddccd5 Lint fix 2025-06-23 23:13:29 -03:00
39abeaaab5 Small fix on workspaceId variable definition on secret-router 2025-06-23 23:05:12 -03:00
198e74cd88 fix: include nooppener in window.open 2025-06-23 18:05:48 -07:00
8ed0a1de84 fix: correct window open for share your own secret link to handle self-hosted 2025-06-23 18:01:38 -07:00
b336c0c3d6 Update secret-folder-service.ts 2025-06-24 03:33:45 +04:00
305f2d79de remove unused path 2025-06-24 03:32:18 +04:00
d4a6faa92c fix(folders): multiple folders being created 2025-06-24 03:24:47 +04:00
4800e9c36e Address PR comments 2025-06-23 17:45:21 -03:00
842a2e9a06 Merge pull request #3834 from Infisical/misc/add-self-serve-for-github-app-connection-setup
misc: add self-serve for github app connection setup
2025-06-24 02:45:51 +08:00
de81d2d380 Merge pull request #3833 from akhilmhdh/feat/pg-queue
feat: migrated dynamic secret to pg queue and corrected service layer
2025-06-23 23:51:06 +05:30
=
f5d769fa05 feat: addressed review comments 2025-06-23 23:38:07 +05:30
b3ace353ce Merge pull request #3843 from Infisical/email-verify-more-aggressive-rate-limit
improvement(verify-endpoints): add more aggressive rate limiting to verify endpoints
2025-06-23 10:43:25 -07:00
48353ab201 Merge pull request #3842 from Infisical/sort-tax-id-dropdown
sort tax ID dropdown
2025-06-23 13:40:01 -04:00
2137d13157 improve key check operator 2025-06-23 10:36:09 -07:00
647e13d654 improvement: add more aggressive rate limiting to verify endpoints 2025-06-23 10:27:36 -07:00
bb2a933a39 sort tax ID dropdown 2025-06-23 13:26:54 -04:00
6f75debb9c Merge pull request #3841 from Infisical/daniel/fix-k8s-dynamic-secret-without-gateway
fix(dynamic-secrets/k8s): fix for SSL when not using gateway
2025-06-23 21:26:20 +04:00
90588bc3c9 fix(dynamic-secrets/k8s): fix for SSL when not using gateway 2025-06-23 21:18:15 +04:00
4a09fc5e63 Merge pull request #3840 from Infisical/doc/added-architecture-doc-for-cloud
doc: architecture for US and EU cloud
2025-06-24 00:53:54 +08:00
f0ec8c883f misc: addressed comments 2025-06-24 00:52:18 +08:00
8024d7448f misc: updated docs json 2025-06-23 22:18:50 +08:00
c65b79e00d Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync 2025-06-23 22:16:09 +08:00
f5238598aa misc: updated admin integration picture 2025-06-23 14:12:54 +00:00
982aa80092 misc: added tabs for admin integrations 2025-06-23 22:05:08 +08:00
c305ddd463 feat(secret-sync): Gitlab PR suggestions 2025-06-23 10:52:59 -03:00
b30706607f misc: changed from for to of 2025-06-23 21:13:59 +08:00
2a3d19dcb2 misc: finalized title 2025-06-23 19:31:19 +08:00
b4ff620b44 doc: removed specifics 2025-06-23 19:28:05 +08:00
23f1888123 misc: added mention of separated AWS accounts 2025-06-23 19:16:08 +08:00
7764f63299 misc: made terms consistent 2025-06-23 19:12:09 +08:00
cb3365afd4 misc: removed troubleshooting section 2025-06-23 19:08:36 +08:00
58705ffc3f doc: removed duplicate permission block 2025-06-23 19:03:50 +08:00
67e57d8993 doc: added mention of NAT 2025-06-23 19:00:45 +08:00
90ff13a6b5 doc: architecture for US and EU cloud 2025-06-23 18:49:26 +08:00
36145a15c1 Merge pull request #3838 from Infisical/docs-update
upgrade mintlify docs
2025-06-23 03:38:53 -04:00
4f64ed6b42 upgrade mintlify docs 2025-06-22 17:25:17 -07:00
27cb686216 feat(secret-sync): Fix frontend file names 2025-06-20 21:26:12 -03:00
e201d77a8f feat(secret-sync): Add gitlab secret sync 2025-06-20 21:13:14 -03:00
d47959ca83 Merge pull request #3822 from Infisical/approval-ui-revisions
improvements(approval-workflows): Improve Approval Workflow Tables and Add Additional Functionality
2025-06-20 15:25:19 -07:00
3b2953ca58 chore: revert license 2025-06-20 12:37:24 -07:00
1daa503e0e improvement: add space to users/groups list label 2025-06-20 12:34:20 -07:00
d69e8d2a8d deconflict merge 2025-06-20 12:33:37 -07:00
7c7af347fc improvements: address feedback and fix bugs 2025-06-20 12:25:28 -07:00
f85efdc6f8 misc: add auto-sync after config update 2025-06-21 02:57:34 +08:00
8680c52412 Merge branch 'misc/add-self-serve-for-github-app-connection-setup' of https://github.com/Infisical/infisical into misc/add-self-serve-for-github-app-connection-setup 2025-06-21 02:41:39 +08:00
0ad3c67f82 misc: minor renames 2025-06-21 02:41:15 +08:00
f75fff0565 doc: add image 2025-06-20 18:31:36 +00:00
1fa1d0a15a misc: add self-serve for github connection setup 2025-06-21 02:23:20 +08:00
e5a967b918 Update license-fns.ts 2025-06-20 23:50:03 +05:30
=
3cfe2223b6 feat: migrated dynamic secret to pg queue and corrected service layer types to non infer version 2025-06-20 23:32:40 +05:30
a43d4fd430 addressed greptie 2025-06-20 21:02:09 +08:00
80b6fb677c misc: addressed url issue 2025-06-20 20:52:00 +08:00
5bc8acd0a7 doc: added api references 2025-06-20 20:46:31 +08:00
2575845df7 misc: added images to secret sync doc 2025-06-20 12:36:39 +00:00
641d58c157 misc: addressed sync overflow issue 2025-06-20 20:23:03 +08:00
430f5d516c misc: text updates to secret sync 2025-06-20 20:20:10 +08:00
5cec194e74 misc: initial cloudflare pages sync doc 2025-06-20 20:17:02 +08:00
5ede4f6f4b misc: added placeholder for account ID 2025-06-20 20:08:07 +08:00
4d3581f835 doc: added assets for app connection 2025-06-20 12:07:21 +00:00
665f7fa5c3 misc: updated account ID 2025-06-20 19:50:03 +08:00
9f4b1d2565 image path updates 2025-06-20 19:42:22 +08:00
59e2a20180 misc: addressed minor issues 2025-06-20 19:39:33 +08:00
4fee5a5839 doc: added initial app connection doc 2025-06-20 19:36:27 +08:00
61e245ea58 Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync 2025-06-20 19:24:45 +08:00
8d6712aa58 Merge pull request #3824 from Infisical/doc/add-helm-install-for-pki-issuer
doc: add mention of helm install for pki issuer
2025-06-20 19:20:19 +08:00
a767870ad6 Merge pull request #3813 from akhilmhdh/patch/min-knex
feat: added min 0 for knexjs pool
2025-06-19 21:16:08 -04:00
a0c432628a Merge pull request #3831 from Infisical/docs/fix-broken-link
Docs links fix
2025-06-19 21:15:22 -04:00
08a74a63b5 Docs links fix 2025-06-19 21:10:58 -04:00
8329240822 Merge pull request #3821 from Infisical/ENG-2832
feat(dynamic-secret): Github App Tokens
2025-06-19 21:03:46 -04:00
ec3cbb9460 Merge pull request #3830 from Infisical/revert-cli-refresh
Revert CLI refresh PR
2025-06-19 20:58:11 -04:00
f167ba0fb8 Revert "Merge pull request #3797 from Infisical/ENG-2690"
This reverts commit 7d90d183fb, reversing
changes made to f385386a4b.
2025-06-19 20:46:55 -04:00
f291aa1c01 Merge pull request #3829 from Infisical/fix/cli-jwt-issue
Revert back to `RefreshToken` from `refreshToken` to support older CLI versions
2025-06-19 19:41:31 -04:00
72131373ec Merge branch 'main' into fix/cli-jwt-issue 2025-06-19 19:19:12 -04:00
16c48de031 refreshToken -> RefreshToken 2025-06-19 19:18:02 -04:00
436a5afab5 Merge pull request #3828 from Infisical/fix/cli-jwt-issue 2025-06-19 19:01:17 -04:00
9445f717f4 Revert back to JTWToken 2025-06-19 18:55:41 -04:00
251e83a3fb Merge pull request #3827 from Infisical/fix/cli-jwt-issue
Fix CLI issue
2025-06-19 17:33:37 -04:00
66df285245 Improvements 2025-06-19 17:26:58 -04:00
73fe2659b5 Fix CLI issue 2025-06-19 17:17:10 -04:00
091f02d1cd Merge pull request #3826 from akhilmhdh/feat/aws-auth-increase-limit
feat: patched up approval sequence ui bugs
2025-06-19 14:15:54 -07:00
57e97a146b feat: cloudflare pages secret sync 2025-06-20 03:43:36 +08:00
66140dc151 Merge pull request #3809 from Infisical/feat/dynamicSecretAwsIamCustomTags
feat(dynamic-secret): Add custom tags to AWS IAM dynamic secret
2025-06-19 16:42:53 -03:00
a8c54d27ef remove debug console logs 2025-06-19 16:19:02 -03:00
9ac4453523 Review fixes 2025-06-19 15:12:41 -04:00
=
a6a9c2404d feat: patched up approval sequence ui bugs 2025-06-20 00:12:49 +05:30
e5352e7aa8 Merge pull request #3806 from Infisical/feat/addHerokuSecretSync
feat(secret-sync): Add Heroku Secret Sync
2025-06-19 15:28:56 -03:00
c52180c890 feat(secret-sync): minor fix on heroku docs 2025-06-19 15:17:36 -03:00
20f0eeed35 Moved tags to aws iam provider inputs 2025-06-19 15:01:35 -03:00
d2c7ed62d0 feat: added cloudflare app connection 2025-06-20 01:16:56 +08:00
7e9743b4c2 improvement: standardize and update server side pagination for change requests 2025-06-19 09:39:42 -07:00
34cf544b3a fix: correct empty state/search logic 2025-06-19 09:39:42 -07:00
12fd063cd5 improvements: minor ui adjustments/additions and pagination for access request table 2025-06-19 09:39:42 -07:00
8fb6063686 improvement: better badge color 2025-06-19 09:39:42 -07:00
459b262865 improvements: improve approval tables UI and add additional functionality 2025-06-19 09:39:42 -07:00
7581300a67 feat(secret-sync): minor fix on heroku sync 2025-06-19 13:38:20 -03:00
7d90d183fb Merge pull request #3797 from Infisical/ENG-2690
feat: Lower token lifetime to 1 day (refresh 14 days) and fix CLI refresh token functionality
2025-06-19 12:05:24 -04:00
f27d4ee973 doc: add mention of helm install for pki issuer 2025-06-19 22:41:39 +08:00
470d7cca6a misc: updated chart version 2025-06-19 20:57:42 +08:00
7473e3e21e Add Heroku PR suggestions 2025-06-19 09:28:43 -03:00
8e3918ada3 misc: addressed tag issue for CLI 2025-06-19 20:20:53 +08:00
6720217cee Merge remote-tracking branch 'origin/main' into feat/addHerokuSecretSync 2025-06-19 08:47:03 -03:00
f385386a4b Merge pull request #3823 from akhilmhdh/feat/aws-auth-increase-limit
feat: resolved okta oidc failing
2025-06-19 07:06:21 -04:00
=
62a0d6e614 feat: corrected the error message 2025-06-19 16:10:15 +05:30
=
8c64c731f9 feat: added additional validation for name 2025-06-19 16:09:22 +05:30
=
d51f6ca4fd feat: resolved okta oidc failing 2025-06-19 16:04:55 +05:30
5abcbe36ca Update oncall-summery-template.mdx 2025-06-18 18:51:48 -04:00
7a13c27055 Greptile review comments and lint 2025-06-18 18:41:58 -04:00
e7ac783b10 feat(dynamic-secret): Github App Tokens 2025-06-18 18:33:11 -04:00
0a509e5033 Merge pull request #3791 from Infisical/feat/add-render-app-connection-and-secret-sync
feat: render app connection and secret sync
2025-06-19 04:49:01 +08:00
bd54054bc3 misc: enabled auto bootstrap for check 2025-06-19 03:53:57 +08:00
cfe51d4a52 misc: improved template dcs 2025-06-19 03:50:56 +08:00
d0c01755fe misc: addressed type issue 2025-06-19 03:29:42 +08:00
41e65775ab misc: addressed comments 2025-06-19 03:24:32 +08:00
e3f4a2e604 Merge pull request #3819 from akhilmhdh/feat/aws-auth-increase-limit
fix: resolved failing duplication of predefined roles
2025-06-19 00:49:18 +05:30
f6e6bdb691 Merge remote-tracking branch 'origin/main' into feat/add-render-app-connection-and-secret-sync 2025-06-19 03:14:23 +08:00
=
819a021e9c feat: corrected enum usage 2025-06-19 00:05:40 +05:30
=
80113c2cea fix: resolved failing duplication of predefined roles 2025-06-19 00:02:17 +05:30
9cdd7380df misc: greptie 2025-06-19 02:30:26 +08:00
07d491acd1 misc: corrected template doc 2025-06-19 02:26:13 +08:00
3276853427 misc: added helm support for auto bootstrap 2025-06-19 02:12:08 +08:00
1f1fb3f3d1 Merge pull request #3817 from akhilmhdh/feat/aws-auth-increase-limit
fix: updated aws principal arn field size to 2048
2025-06-18 23:21:59 +05:30
a8eb72a8c5 Fix type issue 2025-06-18 14:48:29 -03:00
2b8220a71b feat: added support for outputting bootstrap credentials to k8 secret 2025-06-19 01:43:47 +08:00
f76d3e2a14 Add projectSlug parameter on secrets v3 endpoints 2025-06-18 14:35:49 -03:00
=
d35331b0a8 fix: updated aws principal arn field size to 2048 2025-06-18 23:00:52 +05:30
ff6d94cbd0 Merge pull request #3815 from Infisical/daniel/update-dotnet-docs
docs: update .net sdk
2025-06-18 18:55:09 +04:00
=
01ef498397 feat: added min 0 for knexjs pool 2025-06-18 15:16:07 +05:30
59ac14380a Merge pull request #3810 from Infisical/daniel/secret-syncs-permissions
feat(secret-syncs): better permissioning
2025-06-17 21:44:47 -04:00
7b5c86f4ef revert previous commit 2025-06-17 17:34:00 -07:00
a745be2546 improvements: remove secret permission checks from secret syncs 2025-06-17 17:28:21 -07:00
02f311515c feat(secret-sync): Add PR suggestions for Heroku Integration 2025-06-17 21:19:21 -03:00
e8cb3f8b4a improvements: fix secret sync policy parsing, add read checks/filters and disable ui based of conditions 2025-06-17 16:18:41 -07:00
4c8063c532 docs: update .net sdk 2025-06-18 01:51:33 +04:00
6a9b2d3d48 Merge pull request #3804 from Infisical/service-tokens-table-improvements
improvement(service-tokens): Improve Service Tokens Table
2025-06-17 14:15:07 -07:00
0a39e138a1 fix: move service token form to separate component to prevent reset issue 2025-06-17 14:10:48 -07:00
0dce2045ec Merge pull request #3802 from Infisical/ENG-2929
feat(secret-sync, app-connection): Fly.io Secret Sync + App Connection
2025-06-17 16:57:58 -04:00
b4c118d246 requested changes 2025-06-18 00:26:26 +04:00
90e675de1e docs(secret-syncs): add conditions support 2025-06-18 00:22:25 +04:00
741e0ec78f Fixed credential validation 2025-06-17 16:18:35 -04:00
3f654e115d feat(secret-syncs): better permissioning 2025-06-18 00:17:39 +04:00
1921346b4f Review fixes 2025-06-17 16:05:09 -04:00
76c95ace63 Merge branch 'main' into ENG-2929 2025-06-17 15:57:31 -04:00
f4ae40cb86 Merge pull request #3805 from Infisical/access-control-tab-consistency
improvement(project-access-control): minor UI adjustments for consistency
2025-06-17 12:47:11 -07:00
b790dbb36f feat(dynamic-secret): Add tags to AWS IAM docs and add aws key-value limits to the schema 2025-06-17 16:21:29 -03:00
14449b8b41 improvements: address feedback 2025-06-17 12:17:53 -07:00
489bd124d2 feat(dynamic-secret): Add custom tags to AWS IAM dynamic secret 2025-06-17 16:06:35 -03:00
bcdcaa33a4 Merge pull request #3807 from Infisical/conditional-dynamic-secret-access-display
improvement(access-tree): dynamic secret conditional display
2025-06-17 11:49:45 -07:00
e8a8542757 Merge pull request #3803 from Infisical/project-roles-table-improvements
improvement(project-roles): Add pagination, search and column sorting to Project Roles table
2025-06-17 11:49:31 -07:00
e61d35d824 Merge pull request #3808 from Infisical/daniel/fix-editing-value-hidden-secrets
fix: editing secrets with value hidden
2025-06-17 22:08:50 +04:00
714d6831bd Update SecretListView.tsx 2025-06-17 21:23:30 +04:00
956f75eb43 fix: editing secrets with value hidden 2025-06-17 21:02:47 +04:00
73902c3ad6 improvement: hide secret path/enviornment in conditional tooltip 2025-06-17 09:45:56 -07:00
da792d144d improvements: address feedback 2025-06-17 09:29:17 -07:00
f7b09f5fc2 improvement: add conditional display to access tree for dynamic secret metadata 2025-06-17 08:16:39 -07:00
bfee34f38d Merge pull request #3801 from Infisical/doc/production-hardening
doc: production hardening
2025-06-17 22:10:22 +08:00
840b64a049 fix mint.json openapi url used for local test 2025-06-17 10:54:52 -03:00
c2612f242c feat(secret-sync): Add Heroku Secret Sync 2025-06-17 10:52:55 -03:00
092b89c59e misc: corrected kms section 2025-06-17 20:28:28 +08:00
3d76ae3399 misc: some more updates in examples 2025-06-17 20:25:38 +08:00
23aa97feff misc: addressed comments 2025-06-17 20:17:17 +08:00
0c5155f8e6 improvement: minor UI adjustments to make project access control tabs more uniform 2025-06-16 17:17:55 -07:00
796d6bfc85 improvement: add scope handling to service token search 2025-06-16 16:42:11 -07:00
4afe2f2377 improvements: use stored preferred page size for project roles table and add reset helper 2025-06-16 16:36:03 -07:00
6eaa16bd07 improvement: add pagination, search and column sort to service token table and improve table rows 2025-06-16 16:13:09 -07:00
1e07c2fe23 improvements: add sorting, search, and pagination to project roles table and improve dropdown menu 2025-06-16 15:00:40 -07:00
149f98a1b7 Update docs/integrations/secret-syncs/flyio.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-16 16:55:34 -04:00
14745b560c Update docs/integrations/app-connections/flyio.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-16 16:55:22 -04:00
dcfa0a2386 Update docs/integrations/secret-syncs/flyio.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-16 16:55:07 -04:00
199339ac32 Minor schema improvements 2025-06-16 16:28:09 -04:00
2aeb02b74a Fly.io secret sync & app connection docs 2025-06-16 16:26:54 -04:00
fe75627ab7 Fly.io secret sync 2025-06-16 15:49:42 -04:00
191486519f Merge branch 'doc/production-hardening' of https://github.com/Infisical/infisical into doc/production-hardening 2025-06-17 03:45:54 +08:00
cab8fb0d8e misc: reorganized 2025-06-17 03:45:35 +08:00
8bfd728ce4 misc: added mint json 2025-06-16 19:22:35 +00:00
c9eab0af18 misc: updated section on db 2025-06-17 03:21:26 +08:00
d7dfc531fc doc: added guide for production hardening 2025-06-17 03:20:11 +08:00
a89bd08c08 Merge pull request #3795 from Infisical/ENG-2928
feat(machine-identities): Delete protection
2025-06-16 14:57:45 -04:00
4bfb9e8e74 Merge pull request #3789 from Infisical/misc/add-custom-role-slug-in-fetch-group
misc: add custom role slug in fetch group
2025-06-16 22:40:37 +04:00
da5f054a65 Fly.io app connection 2025-06-16 14:08:42 -04:00
9b13619efa Merge pull request #3799 from Infisical/daniel/hotfix-2
Fix: increase PIT tree checkout interval
2025-06-16 20:28:51 +04:00
c076a900dc Update env.ts 2025-06-16 20:27:02 +04:00
8a5279cf0d Merge pull request #3798 from Infisical/daniel/hotfix
fix: increase PIT checkpoint window
2025-06-16 20:09:29 +04:00
d45c29cd23 Update env.ts 2025-06-16 20:08:22 +04:00
77fe2ffb3b Add error handling 2025-06-14 01:43:32 -04:00
edf4e75e55 Spelling fix "JTW" -> "JWT" 2025-06-14 01:38:29 -04:00
de917a5d74 Fix CLI refresh token functionality + reduce token lifetime to 1d & 14d
for refresh
2025-06-14 01:31:44 -04:00
46f9927cf1 Merge pull request #3796 from Infisical/fix/applyWorkspaceLimitToSecretManager
Add a condition to only limit the number of projects to SecretManager
2025-06-13 17:46:35 -03:00
92508d19e6 Add a condition to only limit the number of projects to SecretManager 2025-06-13 17:39:15 -03:00
a73c0c05af Merge pull request #3794 from akhilmhdh/feat/ts-fix
feat: refactoring ts to avoid infer each time
2025-06-14 01:36:27 +05:30
c12bfa766c Review fixes 2025-06-13 14:51:39 -04:00
3432a16d4f Update frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-13 14:45:38 -04:00
19a403f467 feat(machine-identities): Delete protection 2025-06-13 14:37:15 -04:00
=
7a00ade119 feat: updated more types to avoid infer 2025-06-13 23:52:40 +05:30
35127db635 Merge pull request #3793 from Infisical/doc/add-scope-breakdown-section-in-design-doc
doc: add scope breakdown section to design doc
2025-06-13 13:21:45 -04:00
1b9eecc8f4 doc: add scope breakdown section 2025-06-13 17:11:02 +00:00
f0b8c1537c Merge pull request #3788 from Infisical/fix/vercelNotDisplayingAllBranches
fix(secret-sync): allow users to input custom branch names on Vercel
2025-06-13 13:49:00 -03:00
4e60cff4bd feat(secret-sync): Improve Vercel custom option label 2025-06-13 13:41:34 -03:00
ed1100bc90 misc: api references 2025-06-13 23:58:57 +08:00
dabe7e42ec misc: add deprecation for native render integration 2025-06-13 23:52:18 +08:00
c8ca6710ba misc: add secret sync docs 2025-06-13 15:48:59 +00:00
7adac40756 Merge pull request #3790 from Infisical/daniel/approval-ui-fix
fix: hidden secret value in secret approvals
2025-06-13 19:46:35 +04:00
400dc75656 Update SecretApprovalRequestChangeItem.tsx 2025-06-13 19:38:40 +04:00
4ecb2eb383 doc: added docs for render app connection 2025-06-13 15:24:45 +00:00
23a7c1b8cc Merge pull request #3792 from Infisical/fix/alibaba-cloud
Add left join
2025-06-13 11:08:00 -04:00
e51278c276 misc: added max length to apiKey 2025-06-13 23:03:04 +08:00
c014c12ecb misc: addressed frontend lint 2025-06-13 23:01:09 +08:00
097b04afee misc: addressed type 2025-06-13 22:59:08 +08:00
f304024235 add left join 2025-06-13 10:56:30 -04:00
63ccfc40ac feat: added render secret sync 2025-06-13 22:53:35 +08:00
=
5311daed64 feat: checkpoint before disaster strikes 2025-06-13 20:22:38 +05:30
d5e9ac82d0 fix: hidden secret value in secret approvals 2025-06-13 18:45:48 +04:00
b43ecef112 Merge pull request #3786 from Infisical/fix/auth0SamlMappingsTipWarning
fix(docs): replace tip to warning on Auth0 Auth
2025-06-13 10:37:03 -03:00
f9c012387c feat: added render app connection 2025-06-13 20:14:24 +08:00
=
5b51ab3216 feat: updated orm, keystore and queue 2025-06-13 17:40:51 +05:30
b26e56c97e Merge pull request #3777 from akhilmhdh/feat/seq-access-request
feat: Sequentail access approval request
2025-06-13 16:10:17 +05:30
=
7cced29c74 feat: resolved scim failure 2025-06-13 16:04:11 +05:30
06a7e804eb misc: add custom role slug in fetch group 2025-06-13 17:26:36 +08:00
0f00474243 Merge pull request #3735 from Infisical/misc/add-checks-for-helm-verification
misc: add verification pipelines for helm charts
2025-06-12 22:29:44 -04:00
ee98992d9e fix(secret-sync): allow users to input custom branch names as Vercel only send a max of 100 with no pagination available 2025-06-12 20:56:15 -03:00
8c0287681b fix(docs): replace tip to warning on Auth0 Auth 2025-06-12 18:15:44 -03:00
=
d5f0b4dad9 feat: fullstop 2025-06-13 00:30:08 +05:30
=
0fd1b1c9d7 feat: resolved type issue 2025-06-12 23:24:12 +05:30
=
79df946f02 feat: fixed migration issue 2025-06-12 23:00:12 +05:30
=
da2fa7f3ca feat: fixed sort by sequence 2025-06-12 21:42:35 +05:30
=
547ef17c10 feat: corrected validation 2025-06-12 16:01:48 +05:30
=
841408042e feat: ui resolved sequence grouping error 2025-06-12 16:00:45 +05:30
=
e5fb1ac808 feat: updated ui based on review 2025-06-12 15:31:41 +05:30
=
2d4476f99c feat: resolved ts error in rhf 2025-06-11 15:33:32 +05:30
=
81df491d5e feat: reptile feedback and resolved type failure 2025-06-11 15:20:46 +05:30
d2c5603664 Update frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-11 15:20:46 +05:30
=
096930cb8f feat: updated doc and fixed overflow in model for access policy 2025-06-11 15:20:46 +05:30
=
f9c00cf442 feat: ui changes for approval to work 2025-06-11 15:20:46 +05:30
=
d32b6ad41d feat: updated policy to have sequence order 2025-06-11 15:20:45 +05:30
1334 changed files with 39796 additions and 19996 deletions

View File

@ -107,6 +107,10 @@ INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
INF_APP_CONNECTION_GITHUB_APP_SLUG= INF_APP_CONNECTION_GITHUB_APP_SLUG=
INF_APP_CONNECTION_GITHUB_APP_ID= INF_APP_CONNECTION_GITHUB_APP_ID=
#gitlab app connection
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID=
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET=
#github radar app connection #github radar app connection
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID= INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID=
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET= INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET=

View File

@ -83,7 +83,7 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest-8-cores
needs: [cli-integration-tests] needs: [cli-integration-tests]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -51,11 +51,18 @@ jobs:
--from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \ --from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \
--from-literal=SITE_URL=http://localhost:8080 --from-literal=SITE_URL=http://localhost:8080
- name: Create bootstrap secret
run: |
kubectl create secret generic infisical-bootstrap-credentials \
--namespace infisical-standalone-postgres \
--from-literal=INFISICAL_ADMIN_EMAIL=admin@example.com \
--from-literal=INFISICAL_ADMIN_PASSWORD=admin-password
- name: Run chart-testing (install) - name: Run chart-testing (install)
run: | run: |
ct install \ ct install \
--config ct.yaml \ --config ct.yaml \
--charts helm-charts/infisical-standalone-postgres \ --charts helm-charts/infisical-standalone-postgres \
--helm-extra-args="--timeout=300s" \ --helm-extra-args="--timeout=300s" \
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \ --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres --set infisical.autoBootstrap.enabled=true" \
--namespace infisical-standalone-postgres --namespace infisical-standalone-postgres

View File

@ -45,3 +45,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:582
.github/workflows/helm-release-infisical-core.yml:generic-api-key:48 .github/workflows/helm-release-infisical-core.yml:generic-api-key:48
.github/workflows/helm-release-infisical-core.yml:generic-api-key:47 .github/workflows/helm-release-infisical-core.yml:generic-api-key:47
backend/src/services/smtp/smtp-service.ts:generic-api-key:79 backend/src/services/smtp/smtp-service.ts:generic-api-key:79
frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx:cloudflare-api-key:7

View File

@ -8,6 +8,9 @@ import { Lock } from "@app/lib/red-lock";
export const mockKeyStore = (): TKeyStoreFactory => { export const mockKeyStore = (): TKeyStoreFactory => {
const store: Record<string, string | number | Buffer> = {}; const store: Record<string, string | number | Buffer> = {};
const getRegex = (pattern: string) =>
new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
return { return {
setItem: async (key, value) => { setItem: async (key, value) => {
store[key] = value; store[key] = value;
@ -23,7 +26,7 @@ export const mockKeyStore = (): TKeyStoreFactory => {
return 1; return 1;
}, },
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => { deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`); const regex = getRegex(pattern);
let totalDeleted = 0; let totalDeleted = 0;
const keys = Object.keys(store); const keys = Object.keys(store);
@ -53,6 +56,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
incrementBy: async () => { incrementBy: async () => {
return 1; return 1;
}, },
getItems: async (keys) => {
const values = keys.map((key) => {
const value = store[key];
if (typeof value === "string") {
return value;
}
return null;
});
return values;
},
getKeysByPattern: async (pattern) => {
const regex = getRegex(pattern);
const keys = Object.keys(store);
return keys.filter((key) => regex.test(key));
},
deleteItemsByKeyIn: async (keys) => {
for (const key of keys) {
delete store[key];
}
return keys.length;
},
acquireLock: () => { acquireLock: () => {
return Promise.resolve({ return Promise.resolve({
release: () => {} release: () => {}

View File

@ -26,6 +26,7 @@ export const mockQueue = (): TQueueServiceFactory => {
getRepeatableJobs: async () => [], getRepeatableJobs: async () => [],
clearQueue: async () => {}, clearQueue: async () => {},
stopJobById: async () => {}, stopJobById: async () => {},
stopJobByIdPg: async () => {},
stopRepeatableJobByJobId: async () => true, stopRepeatableJobByJobId: async () => true,
stopRepeatableJobByKey: async () => true stopRepeatableJobByKey: async () => true
}; };

View File

@ -30,6 +30,7 @@
"@fastify/static": "^7.0.4", "@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@gitbeaker/rest": "^42.5.0",
"@google-cloud/kms": "^4.5.0", "@google-cloud/kms": "^4.5.0",
"@infisical/quic": "^1.0.8", "@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1", "@node-saml/passport-saml": "^5.0.1",
@ -7807,6 +7808,48 @@
"p-limit": "^3.1.0" "p-limit": "^3.1.0"
} }
}, },
"node_modules/@gitbeaker/core": {
"version": "42.5.0",
"resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-42.5.0.tgz",
"integrity": "sha512-rMWpOPaZi1iLiifnOIoVO57p2EmQQdfIwP4txqNyMvG4WjYP5Ez0U7jRD9Nra41x6K5kTPBZkuQcAdxVWRJcEQ==",
"license": "MIT",
"dependencies": {
"@gitbeaker/requester-utils": "^42.5.0",
"qs": "^6.12.2",
"xcase": "^2.0.1"
},
"engines": {
"node": ">=18.20.0"
}
},
"node_modules/@gitbeaker/requester-utils": {
"version": "42.5.0",
"resolved": "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-42.5.0.tgz",
"integrity": "sha512-HLdLS9LPBMVQumvroQg/4qkphLDtwDB+ygEsrD2u4oYCMUtXV4V1xaVqU4yTXjbTJ5sItOtdB43vYRkBcgueBw==",
"license": "MIT",
"dependencies": {
"picomatch-browser": "^2.2.6",
"qs": "^6.12.2",
"rate-limiter-flexible": "^4.0.1",
"xcase": "^2.0.1"
},
"engines": {
"node": ">=18.20.0"
}
},
"node_modules/@gitbeaker/rest": {
"version": "42.5.0",
"resolved": "https://registry.npmjs.org/@gitbeaker/rest/-/rest-42.5.0.tgz",
"integrity": "sha512-oC5cM6jS7aFOp0luTw5mWSRuMgdxwHRLZQ/aWkI+ETMfsprR/HyxsXfljlMY/XJ/fRxTbRJiodR5Axf66WjO3w==",
"license": "MIT",
"dependencies": {
"@gitbeaker/core": "^42.5.0",
"@gitbeaker/requester-utils": "^42.5.0"
},
"engines": {
"node": ">=18.20.0"
}
},
"node_modules/@google-cloud/kms": { "node_modules/@google-cloud/kms": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
@ -24628,6 +24671,18 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/picomatch-browser": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/picomatch-browser/-/picomatch-browser-2.2.6.tgz",
"integrity": "sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w==",
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": { "node_modules/pify": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@ -25562,6 +25617,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/rate-limiter-flexible": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.1.tgz",
"integrity": "sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==",
"license": "ISC"
},
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
@ -31039,6 +31100,12 @@
} }
} }
}, },
"node_modules/xcase": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/xcase/-/xcase-2.0.1.tgz",
"integrity": "sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==",
"license": "MIT"
},
"node_modules/xml-crypto": { "node_modules/xml-crypto": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz", "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz",

View File

@ -149,6 +149,7 @@
"@fastify/static": "^7.0.4", "@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@gitbeaker/rest": "^42.5.0",
"@google-cloud/kms": "^4.5.0", "@google-cloud/kms": "^4.5.0",
"@infisical/quic": "^1.0.8", "@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1", "@node-saml/passport-saml": "^5.0.1",

View File

@ -3,16 +3,15 @@ import "fastify";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { TUsers } from "@app/db/schemas"; import { TUsers } from "@app/db/schemas";
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types";
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service"; import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-types";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
@ -25,14 +24,13 @@ import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { TPitServiceFactory } from "@app/ee/services/pit/pit-service"; import { TPitServiceFactory } from "@app/ee/services/pit/pit-service";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; import { RateLimitConfiguration, TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-types";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-types";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service"; import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
@ -44,7 +42,7 @@ import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service";
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
@ -76,6 +74,7 @@ import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-a
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service"; import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
@ -220,6 +219,7 @@ declare module "fastify" {
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory; identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory; identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory; identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory;
identityTlsCertAuth: TIdentityTlsCertAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOciAuth: TIdentityOciAuthServiceFactory; identityOciAuth: TIdentityOciAuthServiceFactory;

View File

@ -164,6 +164,9 @@ import {
TIdentityProjectMemberships, TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert, TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate, TIdentityProjectMembershipsUpdate,
TIdentityTlsCertAuths,
TIdentityTlsCertAuthsInsert,
TIdentityTlsCertAuthsUpdate,
TIdentityTokenAuths, TIdentityTokenAuths,
TIdentityTokenAuthsInsert, TIdentityTokenAuthsInsert,
TIdentityTokenAuthsUpdate, TIdentityTokenAuthsUpdate,
@ -794,6 +797,11 @@ declare module "knex/types/tables" {
TIdentityAlicloudAuthsInsert, TIdentityAlicloudAuthsInsert,
TIdentityAlicloudAuthsUpdate TIdentityAlicloudAuthsUpdate
>; >;
[TableName.IdentityTlsCertAuth]: KnexOriginal.CompositeTableType<
TIdentityTlsCertAuths,
TIdentityTlsCertAuthsInsert,
TIdentityTlsCertAuthsUpdate
>;
[TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType< [TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType<
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAwsAuthsInsert, TIdentityAwsAuthsInsert,

View File

@ -1,6 +1,6 @@
import knex, { Knex } from "knex"; import knex, { Knex } from "knex";
export type TDbClient = ReturnType<typeof initDbConnection>; export type TDbClient = Knex;
export const initDbConnection = ({ export const initDbConnection = ({
dbConnectionUri, dbConnectionUri,
dbRootCert, dbRootCert,
@ -50,6 +50,8 @@ export const initDbConnection = ({
} }
: false : false
}, },
// https://knexjs.org/guide/#pool
pool: { min: 0, max: 10 },
migrations: { migrations: {
tableName: "infisical_migrations" tableName: "infisical_migrations"
} }
@ -70,7 +72,8 @@ export const initDbConnection = ({
}, },
migrations: { migrations: {
tableName: "infisical_migrations" tableName: "infisical_migrations"
} },
pool: { min: 0, max: 10 }
}); });
}); });
@ -107,7 +110,8 @@ export const initAuditLogDbConnection = ({
}, },
migrations: { migrations: {
tableName: "infisical_migrations" tableName: "infisical_migrations"
} },
pool: { min: 0, max: 10 }
}); });
// we add these overrides so that auditLogDb and the primary DB are interchangeable // we add these overrides so that auditLogDb and the primary DB are interchangeable

View File

@ -0,0 +1,44 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasStepColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "sequence");
const hasApprovalRequiredColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicyApprover,
"approvalsRequired"
);
if (!hasStepColumn || !hasApprovalRequiredColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (t) => {
if (!hasStepColumn) t.integer("sequence").defaultTo(1);
if (!hasApprovalRequiredColumn) t.integer("approvalsRequired").nullable();
});
}
// set rejected status for all access request that was rejected and still has status pending
const subquery = knex(TableName.AccessApprovalRequest)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequestReviewer}.requestId`,
`${TableName.AccessApprovalRequest}.id`
)
.where(`${TableName.AccessApprovalRequest}.status` as "status", "pending")
.where(`${TableName.AccessApprovalRequestReviewer}.status` as "status", "rejected")
.select(`${TableName.AccessApprovalRequest}.id`);
await knex(TableName.AccessApprovalRequest).where("id", "in", subquery).update("status", "rejected");
}
export async function down(knex: Knex): Promise<void> {
const hasStepColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "sequence");
const hasApprovalRequiredColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicyApprover,
"approvalsRequired"
);
if (hasStepColumn || hasApprovalRequiredColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (t) => {
if (hasStepColumn) t.dropColumn("sequence");
if (hasApprovalRequiredColumn) t.dropColumn("approvalsRequired");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
if (!hasCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.boolean("hasDeleteProtection").notNullable().defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
if (hasCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.dropColumn("hasDeleteProtection");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns");
if (hasColumn) {
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
t.string("allowedPrincipalArns", 2048).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns");
if (hasColumn) {
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
t.string("allowedPrincipalArns", 255).notNullable().alter();
});
}
}

View File

@ -0,0 +1,91 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientId"
);
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientSecret"
);
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionSlug"
);
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionId"
);
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionPrivateKey"
);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (!hasEncryptedGithubAppConnectionClientIdColumn) {
t.binary("encryptedGitHubAppConnectionClientId").nullable();
}
if (!hasEncryptedGithubAppConnectionClientSecretColumn) {
t.binary("encryptedGitHubAppConnectionClientSecret").nullable();
}
if (!hasEncryptedGithubAppConnectionSlugColumn) {
t.binary("encryptedGitHubAppConnectionSlug").nullable();
}
if (!hasEncryptedGithubAppConnectionAppIdColumn) {
t.binary("encryptedGitHubAppConnectionId").nullable();
}
if (!hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
t.binary("encryptedGitHubAppConnectionPrivateKey").nullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientId"
);
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientSecret"
);
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionSlug"
);
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionId"
);
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionPrivateKey"
);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (hasEncryptedGithubAppConnectionClientIdColumn) {
t.dropColumn("encryptedGitHubAppConnectionClientId");
}
if (hasEncryptedGithubAppConnectionClientSecretColumn) {
t.dropColumn("encryptedGitHubAppConnectionClientSecret");
}
if (hasEncryptedGithubAppConnectionSlugColumn) {
t.dropColumn("encryptedGitHubAppConnectionSlug");
}
if (hasEncryptedGithubAppConnectionAppIdColumn) {
t.dropColumn("encryptedGitHubAppConnectionId");
}
if (hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
t.dropColumn("encryptedGitHubAppConnectionPrivateKey");
}
});
}

View File

@ -0,0 +1,28 @@
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.IdentityTlsCertAuth))) {
await knex.schema.createTable(TableName.IdentityTlsCertAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("allowedCommonNames").nullable();
t.binary("encryptedCaCertificate").notNullable();
});
}
await createOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityTlsCertAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
}

View File

@ -0,0 +1,41 @@
import { Knex } from "knex";
import { ProjectType, TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
const hasDefaultTypeColumn = await knex.schema.hasColumn(TableName.Project, "defaultProduct");
if (hasTypeColumn && !hasDefaultTypeColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("type").nullable().alter();
t.string("defaultProduct").notNullable().defaultTo(ProjectType.SecretManager);
});
await knex(TableName.Project).update({
// eslint-disable-next-line
// @ts-ignore this is because this field is created later
defaultProduct: knex.raw(`
CASE
WHEN "type" IS NULL OR "type" = '' THEN 'secret-manager'
ELSE "type"
END
`)
});
}
const hasTemplateTypeColumn = await knex.schema.hasColumn(TableName.ProjectTemplates, "type");
if (hasTemplateTypeColumn) {
await knex.schema.alterTable(TableName.ProjectTemplates, (t) => {
t.string("type").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasDefaultTypeColumn = await knex.schema.hasColumn(TableName.Project, "defaultProduct");
if (hasDefaultTypeColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("defaultProduct");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (!hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.binary("encryptedEnvOverrides").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("encryptedEnvOverrides");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
if (!hasColumn) {
t.datetime("lastInvitedAt").nullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
if (hasColumn) {
t.dropColumn("lastInvitedAt");
}
});
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
if (hasColumn) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.datetime("lastInvitedAt").nullable().defaultTo(knex.fn.now()).alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
if (hasColumn) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.datetime("lastInvitedAt").nullable().alter();
});
}
}

View File

@ -13,7 +13,9 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid().nullable().optional(), approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional() approverGroupId: z.string().uuid().nullable().optional(),
sequence: z.number().default(0).nullable().optional(),
approvalsRequired: z.number().default(1).nullable().optional()
}); });
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>; export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

View File

@ -12,7 +12,8 @@ export const IdentitiesSchema = z.object({
name: z.string(), name: z.string(),
authMethod: z.string().nullable().optional(), authMethod: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
hasDeleteProtection: z.boolean().default(false)
}); });
export type TIdentities = z.infer<typeof IdentitiesSchema>; export type TIdentities = z.infer<typeof IdentitiesSchema>;

View File

@ -0,0 +1,27 @@
// 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 IdentityTlsCertAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
allowedCommonNames: z.string().nullable().optional(),
encryptedCaCertificate: zodBuffer
});
export type TIdentityTlsCertAuths = z.infer<typeof IdentityTlsCertAuthsSchema>;
export type TIdentityTlsCertAuthsInsert = Omit<z.input<typeof IdentityTlsCertAuthsSchema>, TImmutableDBKeys>;
export type TIdentityTlsCertAuthsUpdate = Partial<Omit<z.input<typeof IdentityTlsCertAuthsSchema>, TImmutableDBKeys>>;

View File

@ -52,6 +52,7 @@ export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege"; export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role"; export * from "./identity-project-membership-role";
export * from "./identity-project-memberships"; export * from "./identity-project-memberships";
export * from "./identity-tls-cert-auths";
export * from "./identity-token-auths"; export * from "./identity-token-auths";
export * from "./identity-ua-client-secrets"; export * from "./identity-ua-client-secrets";
export * from "./identity-universal-auths"; export * from "./identity-universal-auths";

View File

@ -86,6 +86,7 @@ export enum TableName {
IdentityOidcAuth = "identity_oidc_auths", IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths", IdentityJwtAuth = "identity_jwt_auths",
IdentityLdapAuth = "identity_ldap_auths", IdentityLdapAuth = "identity_ldap_auths",
IdentityTlsCertAuth = "identity_tls_cert_auths",
IdentityOrgMembership = "identity_org_memberships", IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectMembershipRole = "identity_project_membership_role",
@ -251,6 +252,7 @@ export enum IdentityAuthMethod {
ALICLOUD_AUTH = "alicloud-auth", ALICLOUD_AUTH = "alicloud-auth",
AWS_AUTH = "aws-auth", AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth", AZURE_AUTH = "azure-auth",
TLS_CERT_AUTH = "tls-cert-auth",
OCI_AUTH = "oci-auth", OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth", OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth", JWT_AUTH = "jwt-auth",
@ -265,16 +267,6 @@ export enum ProjectType {
SecretScanning = "secret-scanning" SecretScanning = "secret-scanning"
} }
export enum ActionProjectType {
SecretManager = ProjectType.SecretManager,
CertificateManager = ProjectType.CertificateManager,
KMS = ProjectType.KMS,
SSH = ProjectType.SSH,
SecretScanning = ProjectType.SecretScanning,
// project operations that happen on all types
Any = "any"
}
export enum SortDirection { export enum SortDirection {
ASC = "asc", ASC = "asc",
DESC = "desc" DESC = "desc"

View File

@ -18,7 +18,8 @@ export const OrgMembershipsSchema = z.object({
orgId: z.string().uuid(), orgId: z.string().uuid(),
roleId: z.string().uuid().nullable().optional(), roleId: z.string().uuid().nullable().optional(),
projectFavorites: z.string().array().nullable().optional(), projectFavorites: z.string().array().nullable().optional(),
isActive: z.boolean().default(true) isActive: z.boolean().default(true),
lastInvitedAt: z.date().nullable().optional()
}); });
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>; export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;

View File

@ -16,7 +16,7 @@ export const ProjectTemplatesSchema = z.object({
orgId: z.string().uuid(), orgId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
type: z.string().default("secret-manager") type: z.string().nullable().optional()
}); });
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>; export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;

View File

@ -25,11 +25,12 @@ export const ProjectsSchema = z.object({
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
type: z.string(), type: z.string().nullable().optional(),
enforceCapitalization: z.boolean().default(false), enforceCapitalization: z.boolean().default(false),
hasDeleteProtection: z.boolean().default(false).nullable().optional(), hasDeleteProtection: z.boolean().default(false).nullable().optional(),
secretSharing: z.boolean().default(true), secretSharing: z.boolean().default(true),
showSnapshotsLegacy: z.boolean().default(false) showSnapshotsLegacy: z.boolean().default(false),
defaultProduct: z.string().default("secret-manager")
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@ -29,7 +29,13 @@ export const SuperAdminSchema = z.object({
adminIdentityIds: z.string().array().nullable().optional(), adminIdentityIds: z.string().array().nullable().optional(),
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional() encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(),
encryptedEnvOverrides: zodBuffer.nullable().optional()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -23,12 +23,26 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
environment: z.string(), environment: z.string(),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() }) type: z.literal(ApproverType.Group),
id: z.string(),
sequence: z.number().int().default(1)
}),
z.object({
type: z.literal(ApproverType.User),
id: z.string().optional(),
username: z.string().optional(),
sequence: z.number().int().default(1)
})
]) ])
.array() .array()
.max(100, "Cannot have more than 100 approvers") .max(100, "Cannot have more than 100 approvers")
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" })
.refine(
// @ts-expect-error this is ok
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
"Must provide either username or id"
),
bypassers: z bypassers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }), z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
@ -37,6 +51,13 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.array() .array()
.max(100, "Cannot have more than 100 bypassers") .max(100, "Cannot have more than 100 bypassers")
.optional(), .optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
})
.array()
.optional(),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true)
@ -78,7 +99,12 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: sapPubSchema approvals: sapPubSchema
.extend({ .extend({
approvers: z approvers: z
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() }) .object({
type: z.nativeEnum(ApproverType),
id: z.string().nullable().optional(),
sequence: z.number().nullable().optional(),
approvalsRequired: z.number().nullable().optional()
})
.array() .array()
.nullable() .nullable()
.optional(), .optional(),
@ -152,12 +178,26 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.transform((val) => (val === "" ? "/" : val)), .transform((val) => (val === "" ? "/" : val)),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() }) type: z.literal(ApproverType.Group),
id: z.string(),
sequence: z.number().int().default(1)
}),
z.object({
type: z.literal(ApproverType.User),
id: z.string().optional(),
username: z.string().optional(),
sequence: z.number().int().default(1)
})
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }) .min(1, { message: "At least one approver should be provided" })
.max(100, "Cannot have more than 100 approvers"), .max(100, "Cannot have more than 100 approvers")
.refine(
// @ts-expect-error this is ok
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
"Must provide either username or id"
),
bypassers: z bypassers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }), z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
@ -168,7 +208,14 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.optional(), .optional(),
approvals: z.number().min(1).optional(), approvals: z.number().min(1).optional(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
})
.array()
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -235,7 +282,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.object({ .object({
type: z.nativeEnum(ApproverType), type: z.nativeEnum(ApproverType),
id: z.string().nullable().optional(), id: z.string().nullable().optional(),
name: z.string().nullable().optional() name: z.string().nullable().optional(),
approvalsRequired: z.number().nullable().optional()
}) })
.array() .array()
.nullable() .nullable()

View File

@ -60,7 +60,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
method: "GET", method: "GET",
schema: { schema: {
querystring: z.object({ querystring: z.object({
projectSlug: z.string().trim() projectSlug: z.string().trim(),
policyId: z.string().trim().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -73,6 +74,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
handler: async (req) => { handler: async (req) => {
const { count } = await server.services.accessApprovalRequest.getCount({ const { count } = await server.services.accessApprovalRequest.getCount({
projectSlug: req.query.projectSlug, projectSlug: req.query.projectSlug,
policyId: req.query.policyId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
@ -89,7 +91,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
schema: { schema: {
querystring: z.object({ querystring: z.object({
projectSlug: z.string().trim(), projectSlug: z.string().trim(),
authorProjectMembershipId: z.string().trim().optional(), authorUserId: z.string().trim().optional(),
envSlug: z.string().trim().optional() envSlug: z.string().trim().optional()
}), }),
response: { response: {
@ -112,7 +114,15 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
approvals: z.number(), approvals: z.number(),
approvers: z.string().array(), approvers: z
.object({
userId: z.string().nullable().optional(),
sequence: z.number().nullable().optional(),
approvalsRequired: z.number().nullable().optional(),
email: z.string().nullable().optional(),
username: z.string().nullable().optional()
})
.array(),
bypassers: z.string().array(), bypassers: z.string().array(),
secretPath: z.string().nullish(), secretPath: z.string().nullish(),
envId: z.string(), envId: z.string(),
@ -135,7 +145,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
handler: async (req) => { handler: async (req) => {
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({ const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
projectSlug: req.query.projectSlug, projectSlug: req.query.projectSlug,
authorProjectMembershipId: req.query.authorProjectMembershipId, authorUserId: req.query.authorUserId,
envSlug: req.query.envSlug, envSlug: req.query.envSlug,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,

View File

@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(GROUPS.GET_BY_ID.id) id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
}), }),
response: { response: {
200: GroupsSchema 200: GroupsSchema.extend({
customRoleSlug: z.string().nullable()
})
} }
}, },
handler: async (req) => { handler: async (req) => {

View File

@ -17,6 +17,7 @@ import { z } from "zod";
import { LdapGroupMapsSchema } from "@app/db/schemas"; import { LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { ApiDocsTags, LdapSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -132,10 +133,18 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Get LDAP config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
organizationId: z.string().trim() organizationId: z.string().trim().describe(LdapSso.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -172,23 +181,32 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Create LDAP config",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
organizationId: z.string().trim(), organizationId: z.string().trim().describe(LdapSso.CREATE_CONFIG.organizationId),
isActive: z.boolean(), isActive: z.boolean().describe(LdapSso.CREATE_CONFIG.isActive),
url: z.string().trim(), url: z.string().trim().describe(LdapSso.CREATE_CONFIG.url),
bindDN: z.string().trim(), bindDN: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindDN),
bindPass: z.string().trim(), bindPass: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim().default("uidNumber"), uniqueUserAttribute: z.string().trim().default("uidNumber").describe(LdapSso.CREATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim(), searchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.searchBase),
searchFilter: z.string().trim().default("(uid={{username}})"), searchFilter: z.string().trim().default("(uid={{username}})").describe(LdapSso.CREATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim(), groupSearchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.groupSearchBase),
groupSearchFilter: z groupSearchFilter: z
.string() .string()
.trim() .trim()
.default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"), .default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))")
caCert: z.string().trim().default("") .describe(LdapSso.CREATE_CONFIG.groupSearchFilter),
caCert: z.string().trim().default("").describe(LdapSso.CREATE_CONFIG.caCert)
}), }),
response: { response: {
200: SanitizedLdapConfigSchema 200: SanitizedLdapConfigSchema
@ -214,23 +232,31 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Update LDAP config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
isActive: z.boolean(), isActive: z.boolean().describe(LdapSso.UPDATE_CONFIG.isActive),
url: z.string().trim(), url: z.string().trim().describe(LdapSso.UPDATE_CONFIG.url),
bindDN: z.string().trim(), bindDN: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindDN),
bindPass: z.string().trim(), bindPass: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim(), uniqueUserAttribute: z.string().trim().describe(LdapSso.UPDATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim(), searchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchBase),
searchFilter: z.string().trim(), searchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim(), groupSearchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchBase),
groupSearchFilter: z.string().trim(), groupSearchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchFilter),
caCert: z.string().trim() caCert: z.string().trim().describe(LdapSso.UPDATE_CONFIG.caCert)
}) })
.partial() .partial()
.merge(z.object({ organizationId: z.string() })), .merge(z.object({ organizationId: z.string().trim().describe(LdapSso.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedLdapConfigSchema 200: SanitizedLdapConfigSchema
} }

View File

@ -13,6 +13,7 @@ import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas"; import { OidcConfigsSchema } from "@app/db/schemas";
import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { ApiDocsTags, OidcSSo } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -153,10 +154,18 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Get OIDC config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
orgSlug: z.string().trim() organizationId: z.string().trim().describe(OidcSSo.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: SanitizedOidcConfigSchema.pick({ 200: SanitizedOidcConfigSchema.pick({
@ -180,9 +189,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { orgSlug } = req.query;
const oidc = await server.services.oidc.getOidc({ const oidc = await server.services.oidc.getOidc({
orgSlug, organizationId: req.query.organizationId,
type: "external", type: "external",
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@ -200,8 +208,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Update OIDC config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
allowedEmailDomains: z allowedEmailDomains: z
@ -216,22 +232,26 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",") .split(",")
.map((id) => id.trim()) .map((id) => id.trim())
.join(", "); .join(", ");
}), })
discoveryURL: z.string().trim(), .describe(OidcSSo.UPDATE_CONFIG.allowedEmailDomains),
configurationType: z.nativeEnum(OIDCConfigurationType), discoveryURL: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.discoveryURL),
issuer: z.string().trim(), configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.UPDATE_CONFIG.configurationType),
authorizationEndpoint: z.string().trim(), issuer: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.issuer),
jwksUri: z.string().trim(), authorizationEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.authorizationEndpoint),
tokenEndpoint: z.string().trim(), jwksUri: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.jwksUri),
userinfoEndpoint: z.string().trim(), tokenEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.tokenEndpoint),
clientId: z.string().trim(), userinfoEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.userinfoEndpoint),
clientSecret: z.string().trim(), clientId: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientId),
isActive: z.boolean(), clientSecret: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientSecret),
manageGroupMemberships: z.boolean().optional(), isActive: z.boolean().describe(OidcSSo.UPDATE_CONFIG.isActive),
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional() manageGroupMemberships: z.boolean().optional().describe(OidcSSo.UPDATE_CONFIG.manageGroupMemberships),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.describe(OidcSSo.UPDATE_CONFIG.jwtSignatureAlgorithm)
}) })
.partial() .partial()
.merge(z.object({ orgSlug: z.string() })), .merge(z.object({ organizationId: z.string().describe(OidcSSo.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedOidcConfigSchema.pick({ 200: SanitizedOidcConfigSchema.pick({
id: true, id: true,
@ -267,8 +287,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Create OIDC config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
allowedEmailDomains: z allowedEmailDomains: z
@ -283,23 +311,34 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",") .split(",")
.map((id) => id.trim()) .map((id) => id.trim())
.join(", "); .join(", ");
}), })
configurationType: z.nativeEnum(OIDCConfigurationType), .describe(OidcSSo.CREATE_CONFIG.allowedEmailDomains),
issuer: z.string().trim().optional().default(""), configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.CREATE_CONFIG.configurationType),
discoveryURL: z.string().trim().optional().default(""), issuer: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.issuer),
authorizationEndpoint: z.string().trim().optional().default(""), discoveryURL: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.discoveryURL),
jwksUri: z.string().trim().optional().default(""), authorizationEndpoint: z
tokenEndpoint: z.string().trim().optional().default(""), .string()
userinfoEndpoint: z.string().trim().optional().default(""), .trim()
clientId: z.string().trim(), .optional()
clientSecret: z.string().trim(), .default("")
isActive: z.boolean(), .describe(OidcSSo.CREATE_CONFIG.authorizationEndpoint),
orgSlug: z.string().trim(), jwksUri: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.jwksUri),
manageGroupMemberships: z.boolean().optional().default(false), tokenEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.tokenEndpoint),
userinfoEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.userinfoEndpoint),
clientId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientId),
clientSecret: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientSecret),
isActive: z.boolean().describe(OidcSSo.CREATE_CONFIG.isActive),
organizationId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.organizationId),
manageGroupMemberships: z
.boolean()
.optional()
.default(false)
.describe(OidcSSo.CREATE_CONFIG.manageGroupMemberships),
jwtSignatureAlgorithm: z jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm) .nativeEnum(OIDCJWTSignatureAlgorithm)
.optional() .optional()
.default(OIDCJWTSignatureAlgorithm.RS256) .default(OIDCJWTSignatureAlgorithm.RS256)
.describe(OidcSSo.CREATE_CONFIG.jwtSignatureAlgorithm)
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) { if (data.configurationType === OIDCConfigurationType.CUSTOM) {

View File

@ -111,15 +111,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId) workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
}), }),
querystring: z.object({ querystring: z
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), .object({
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType), eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit), offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor) limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
}), actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
})
.superRefine((el, ctx) => {
if (el.endDate && el.startDate) {
const startDate = new Date(el.startDate);
const endDate = new Date(el.endDate);
const maxAllowedDate = new Date(startDate);
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
if (endDate < startDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "End date cannot be before start date"
});
}
if (endDate > maxAllowedDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "Dates must be within 3 months"
});
}
}
}),
response: { response: {
200: z.object({ 200: z.object({
auditLogs: AuditLogsSchema.omit({ auditLogs: AuditLogsSchema.omit({
@ -161,7 +184,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
filter: { filter: {
...req.query, ...req.query,
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
endDate: req.query.endDate, endDate: req.query.endDate || new Date().toISOString(),
startDate: req.query.startDate || getLastMidnightDateISO(), startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor, auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined eventType: req.query.eventType ? [req.query.eventType] : undefined

View File

@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema, ProjectType } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
@ -104,9 +104,6 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
hide: false, hide: false,
tags: [ApiDocsTags.ProjectTemplates], tags: [ApiDocsTags.ProjectTemplates],
description: "List project templates for the current organization.", description: "List project templates for the current organization.",
querystring: z.object({
type: z.nativeEnum(ProjectType).optional().describe(ProjectTemplates.LIST.type)
}),
response: { response: {
200: z.object({ 200: z.object({
projectTemplates: SanitizedProjectTemplateSchema.array() projectTemplates: SanitizedProjectTemplateSchema.array()
@ -115,8 +112,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { type } = req.query; const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission, type);
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name)); const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
@ -188,7 +184,6 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
tags: [ApiDocsTags.ProjectTemplates], tags: [ApiDocsTags.ProjectTemplates],
description: "Create a project template.", description: "Create a project template.",
body: z.object({ body: z.object({
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
name: slugSchema({ field: "name" }) name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), { .refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.` message: `The requested project template name is reserved.`
@ -284,7 +279,6 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
tags: [ApiDocsTags.ProjectTemplates], tags: [ApiDocsTags.ProjectTemplates],
description: "Delete a project template.", description: "Delete a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }), params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }),
response: { response: {
200: z.object({ 200: z.object({
projectTemplate: SanitizedProjectTemplateSchema projectTemplate: SanitizedProjectTemplateSchema

View File

@ -13,6 +13,7 @@ import { FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types"; import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types";
import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -149,8 +150,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
firstName, firstName,
lastName: lastName as string, lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string, orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
metadata: userMetadata metadata: userMetadata
}); });
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
@ -262,25 +263,31 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Get SAML config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
organizationId: z.string().trim() organizationId: z.string().trim().describe(SamlSso.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: z 200: z.object({
.object({ id: z.string(),
id: z.string(), organization: z.string(),
organization: z.string(), orgId: z.string(),
orgId: z.string(), authProvider: z.string(),
authProvider: z.string(), isActive: z.boolean(),
isActive: z.boolean(), entryPoint: z.string(),
entryPoint: z.string(), issuer: z.string(),
issuer: z.string(), cert: z.string(),
cert: z.string(), lastUsed: z.date().nullable().optional()
lastUsed: z.date().nullable().optional() })
})
.optional()
} }
}, },
handler: async (req) => { handler: async (req) => {
@ -302,15 +309,23 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Create SAML config",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
organizationId: z.string(), organizationId: z.string().trim().describe(SamlSso.CREATE_CONFIG.organizationId),
authProvider: z.nativeEnum(SamlProviders), authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.CREATE_CONFIG.authProvider),
isActive: z.boolean(), isActive: z.boolean().describe(SamlSso.CREATE_CONFIG.isActive),
entryPoint: z.string(), entryPoint: z.string().trim().describe(SamlSso.CREATE_CONFIG.entryPoint),
issuer: z.string(), issuer: z.string().trim().describe(SamlSso.CREATE_CONFIG.issuer),
cert: z.string() cert: z.string().trim().describe(SamlSso.CREATE_CONFIG.cert)
}), }),
response: { response: {
200: SanitizedSamlConfigSchema 200: SanitizedSamlConfigSchema
@ -341,18 +356,26 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Update SAML config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
authProvider: z.nativeEnum(SamlProviders), authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.UPDATE_CONFIG.authProvider),
isActive: z.boolean(), isActive: z.boolean().describe(SamlSso.UPDATE_CONFIG.isActive),
entryPoint: z.string(), entryPoint: z.string().trim().describe(SamlSso.UPDATE_CONFIG.entryPoint),
issuer: z.string(), issuer: z.string().trim().describe(SamlSso.UPDATE_CONFIG.issuer),
cert: z.string() cert: z.string().trim().describe(SamlSso.UPDATE_CONFIG.cert)
}) })
.partial() .partial()
.merge(z.object({ organizationId: z.string() })), .merge(z.object({ organizationId: z.string().trim().describe(SamlSso.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedSamlConfigSchema 200: SanitizedSamlConfigSchema
} }

View File

@ -270,7 +270,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}), }),
body: z.object({ body: z.object({
schemas: z.array(z.string()), schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(), userName: z.string().trim(),
name: z name: z
.object({ .object({
@ -278,7 +277,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
givenName: z.string().trim().optional() givenName: z.string().trim().optional()
}) })
.optional(), .optional(),
displayName: z.string().trim(),
emails: z emails: z
.array( .array(
z.object({ z.object({

View File

@ -30,6 +30,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim().optional(), environment: z.string().trim().optional(),
committer: z.string().trim().optional(), committer: z.string().trim().optional(),
search: z.string().trim().optional(),
status: z.nativeEnum(RequestState).optional(), status: z.nativeEnum(RequestState).optional(),
limit: z.coerce.number().default(20), limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0) offset: z.coerce.number().default(0)
@ -66,13 +67,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
userId: z.string().nullable().optional() userId: z.string().nullable().optional()
}) })
.array() .array()
}).array() }).array(),
totalCount: z.number()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({ const { approvals, totalCount } = await server.services.secretApprovalRequest.getSecretApprovals({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@ -80,7 +82,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
...req.query, ...req.query,
projectId: req.query.workspaceId projectId: req.query.workspaceId
}); });
return { approvals }; return { approvals, totalCount };
} }
}); });
@ -92,7 +94,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
}, },
schema: { schema: {
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim(),
policyId: z.string().trim().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -110,7 +113,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId projectId: req.query.workspaceId,
policyId: req.query.policyId
}); });
return { approvals }; return { approvals };
} }
@ -285,6 +289,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
commits: secretRawSchema commits: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true }) .omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
isRotatedSecret: z.boolean().optional(), isRotatedSecret: z.boolean().optional(),
op: z.string(), op: z.string(),
@ -296,6 +301,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
version: z.number(), version: z.number(),
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
secretValueHidden: z.boolean(),
secretComment: z.string().optional() secretComment: z.string().optional()
}) })
.optional() .optional()
@ -306,6 +312,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
version: z.number(), version: z.number(),
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
secretValueHidden: z.boolean(),
secretComment: z.string().optional(), secretComment: z.string().optional(),
tags: SanitizedTagSchema.array().optional(), tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish() secretMetadata: ResourceMetadataSchema.nullish()

View File

@ -80,6 +80,7 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SignSshKey, event: PostHogEventTypes.SignSshKey,
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: { properties: {
certificateTemplateId: req.body.certificateTemplateId, certificateTemplateId: req.body.certificateTemplateId,
principals: req.body.principals, principals: req.body.principals,
@ -171,6 +172,7 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IssueSshCreds, event: PostHogEventTypes.IssueSshCreds,
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: { properties: {
certificateTemplateId: req.body.certificateTemplateId, certificateTemplateId: req.body.certificateTemplateId,
principals: req.body.principals, principals: req.body.principals,

View File

@ -358,6 +358,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IssueSshHostUserCert, event: PostHogEventTypes.IssueSshHostUserCert,
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: { properties: {
sshHostId: req.params.sshHostId, sshHostId: req.params.sshHostId,
hostname: host.hostname, hostname: host.hostname,
@ -427,6 +428,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IssueSshHostHostCert, event: PostHogEventTypes.IssueSshHostHostCert,
organizationId: req.permission.orgId,
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
sshHostId: req.params.sshHostId, sshHostId: req.params.sshHostId,

View File

@ -1,15 +1,15 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { ormify, TOrmify } from "@app/lib/knex";
export type TAccessApprovalPolicyApproverDALFactory = ReturnType<typeof accessApprovalPolicyApproverDALFactory>; export type TAccessApprovalPolicyApproverDALFactory = TOrmify<TableName.AccessApprovalPolicyApprover>;
export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => { export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => {
const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover); const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover);
return { ...accessApprovalPolicyApproverOrm }; return { ...accessApprovalPolicyApproverOrm };
}; };
export type TAccessApprovalPolicyBypasserDALFactory = ReturnType<typeof accessApprovalPolicyBypasserDALFactory>; export type TAccessApprovalPolicyBypasserDALFactory = TOrmify<TableName.AccessApprovalPolicyBypasser>;
export const accessApprovalPolicyBypasserDALFactory = (db: TDbClient) => { export const accessApprovalPolicyBypasserDALFactory = (db: TDbClient) => {
const accessApprovalPolicyBypasserOrm = ormify(db, TableName.AccessApprovalPolicyBypasser); const accessApprovalPolicyBypasserOrm = ormify(db, TableName.AccessApprovalPolicyBypasser);

View File

@ -3,13 +3,363 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies, TUsers } from "@app/db/schemas"; import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies, TUsers } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter, TOrmify } from "@app/lib/knex";
import { ApproverType, BypasserType } from "./access-approval-policy-types"; import {
ApproverType,
BypasserType,
TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy,
TGetAccessApprovalPolicyByIdDTO,
TGetAccessPolicyCountByEnvironmentDTO,
TListAccessApprovalPoliciesDTO,
TUpdateAccessApprovalPolicy
} from "./access-approval-policy-types";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>; export interface TAccessApprovalPolicyDALFactory
extends Omit<TOrmify<TableName.AccessApprovalPolicy>, "findById" | "find"> {
find: (
filter: TFindFilter<
TAccessApprovalPolicies & {
projectId: string;
}
>,
customFilter?: {
policyId?: string;
},
tx?: Knex
) => Promise<
{
approvers: (
| {
id: string | null | undefined;
type: ApproverType.User;
name: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
| {
id: string | null | undefined;
type: ApproverType.Group;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
)[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
bypassers: (
| {
id: string | null | undefined;
type: BypasserType.User;
name: string;
}
| {
id: string | null | undefined;
type: BypasserType.Group;
}
)[];
}[]
>;
findById: (
policyId: string,
tx?: Knex
) => Promise<
| {
approvers: {
id: string | null | undefined;
type: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
}
| undefined
>;
softDeleteById: (
policyId: string,
tx?: Knex
) => Promise<{
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
}>;
findLastValidPolicy: (
{
envId,
secretPath
}: {
envId: string;
secretPath: string;
},
tx?: Knex
) => Promise<
| {
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
}
| undefined
>;
}
export const accessApprovalPolicyDALFactory = (db: TDbClient) => { export interface TAccessApprovalPolicyServiceFactory {
getAccessPolicyCountByEnvSlug: ({
actor,
actorOrgId,
actorAuthMethod,
projectSlug,
actorId,
envSlug
}: TGetAccessPolicyCountByEnvironmentDTO) => Promise<{
count: number;
}>;
createAccessApprovalPolicy: ({
name,
actor,
actorId,
actorOrgId,
secretPath,
actorAuthMethod,
approvals,
approvers,
bypassers,
projectSlug,
environment,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
}: TCreateAccessApprovalPolicy) => Promise<{
environment: {
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
projectId: string;
slug: string;
position: number;
};
projectId: string;
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
}>;
deleteAccessApprovalPolicy: ({
policyId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TDeleteAccessApprovalPolicy) => Promise<{
approvers: {
id: string | null | undefined;
type: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
}>;
updateAccessApprovalPolicy: ({
policyId,
approvers,
bypassers,
secretPath,
name,
actorId,
actor,
actorOrgId,
actorAuthMethod,
approvals,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
}: TUpdateAccessApprovalPolicy) => Promise<{
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
}>;
getAccessApprovalPolicyByProjectSlug: ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectSlug
}: TListAccessApprovalPoliciesDTO) => Promise<
{
approvers: (
| {
id: string | null | undefined;
type: ApproverType;
name: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
| {
id: string | null | undefined;
type: ApproverType;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
)[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
bypassers: (
| {
id: string | null | undefined;
type: BypasserType;
name: string;
}
| {
id: string | null | undefined;
type: BypasserType;
}
)[];
}[]
>;
getAccessApprovalPolicyById: ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
policyId
}: TGetAccessApprovalPolicyByIdDTO) => Promise<{
approvers: (
| {
id: string | null | undefined;
type: ApproverType.User;
name: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
| {
id: string | null | undefined;
type: ApproverType.Group;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
)[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
bypassers: (
| {
id: string | null | undefined;
type: BypasserType.User;
name: string;
}
| {
id: string | null | undefined;
type: BypasserType.Group;
}
)[];
}>;
}
export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPolicyDALFactory => {
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
const accessApprovalPolicyFindQuery = async ( const accessApprovalPolicyFindQuery = async (
@ -48,6 +398,8 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
.select(tx.ref("username").withSchema("bypasserUsers").as("bypasserUsername")) .select(tx.ref("username").withSchema("bypasserUsers").as("bypasserUsername"))
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
.select(tx.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser)) .select(tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser)) .select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName")) .select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
@ -59,7 +411,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
return result; return result;
}; };
const findById = async (policyId: string, tx?: Knex) => { const findById: TAccessApprovalPolicyDALFactory["findById"] = async (policyId, tx) => {
try { try {
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), { const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId [`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
@ -80,35 +432,37 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId: id }) => ({ mapper: ({ approverUserId: id, approverSequence, approvalsRequired }) => ({
id, id,
type: "user" type: "user",
sequence: approverSequence,
approvalsRequired
}) })
}, },
{ {
key: "approverGroupId", key: "approverGroupId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({ mapper: ({ approverGroupId: id, approverSequence, approvalsRequired }) => ({
id, id,
type: "group" type: "group",
sequence: approverSequence,
approvalsRequired
}) })
} }
] ]
}); });
if (!formattedDoc?.[0]) return;
return formattedDoc?.[0]; return {
...formattedDoc?.[0],
approvers: formattedDoc?.[0]?.approvers.sort((a, b) => (a.sequence || 1) - (b.sequence || 1))
};
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindById" }); throw new DatabaseError({ error, name: "FindById" });
} }
}; };
const find = async ( const find: TAccessApprovalPolicyDALFactory["find"] = async (filter, customFilter, tx) => {
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: {
policyId?: string;
},
tx?: Knex
) => {
try { try {
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter); const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
@ -129,18 +483,22 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId: id, approverUsername }) => ({ mapper: ({ approverUserId: id, approverUsername, approverSequence, approvalsRequired }) => ({
id, id,
type: ApproverType.User, type: ApproverType.User as const,
name: approverUsername name: approverUsername,
sequence: approverSequence,
approvalsRequired
}) })
}, },
{ {
key: "approverGroupId", key: "approverGroupId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({ mapper: ({ approverGroupId: id, approverSequence, approvalsRequired }) => ({
id, id,
type: ApproverType.Group type: ApproverType.Group as const,
sequence: approverSequence,
approvalsRequired
}) })
}, },
{ {
@ -148,7 +506,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
label: "bypassers" as const, label: "bypassers" as const,
mapper: ({ bypasserUserId: id, bypasserUsername }) => ({ mapper: ({ bypasserUserId: id, bypasserUsername }) => ({
id, id,
type: BypasserType.User, type: BypasserType.User as const,
name: bypasserUsername name: bypasserUsername
}) })
}, },
@ -157,24 +515,30 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
label: "bypassers" as const, label: "bypassers" as const,
mapper: ({ bypasserGroupId: id }) => ({ mapper: ({ bypasserGroupId: id }) => ({
id, id,
type: BypasserType.Group type: BypasserType.Group as const
}) })
} }
] ]
}); });
return formattedDocs; return formattedDocs.map((el) => ({
...el,
approvers: el?.approvers.sort((a, b) => (a.sequence || 1) - (b.sequence || 1))
}));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find" }); throw new DatabaseError({ error, name: "Find" });
} }
}; };
const softDeleteById = async (policyId: string, tx?: Knex) => { const softDeleteById: TAccessApprovalPolicyDALFactory["softDeleteById"] = async (policyId, tx) => {
const softDeletedPolicy = await accessApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx); const softDeletedPolicy = await accessApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx);
return softDeletedPolicy; return softDeletedPolicy;
}; };
const findLastValidPolicy = async ({ envId, secretPath }: { envId: string; secretPath: string }, tx?: Knex) => { const findLastValidPolicy: TAccessApprovalPolicyDALFactory["findLastValidPolicy"] = async (
{ envId, secretPath },
tx
) => {
try { try {
const result = await (tx || db.replicaNode())(TableName.AccessApprovalPolicy) const result = await (tx || db.replicaNode())(TableName.AccessApprovalPolicy)
.where( .where(

View File

@ -1,9 +1,9 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@ -23,9 +23,8 @@ import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { import {
ApproverType, ApproverType,
BypasserType, BypasserType,
TCreateAccessApprovalPolicy, TAccessApprovalPolicyServiceFactory,
TDeleteAccessApprovalPolicy, TDeleteAccessApprovalPolicy,
TGetAccessApprovalPolicyByIdDTO,
TGetAccessPolicyCountByEnvironmentDTO, TGetAccessPolicyCountByEnvironmentDTO,
TListAccessApprovalPoliciesDTO, TListAccessApprovalPoliciesDTO,
TUpdateAccessApprovalPolicy TUpdateAccessApprovalPolicy
@ -41,14 +40,12 @@ type TAccessApprovalPolicyServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory; groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">; userDAL: Pick<TUserDALFactory, "find">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">; accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find" | "resetReviewByPolicyId">;
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">; accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
}; };
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
@ -62,8 +59,8 @@ export const accessApprovalPolicyServiceFactory = ({
additionalPrivilegeDAL, additionalPrivilegeDAL,
accessApprovalRequestReviewerDAL, accessApprovalRequestReviewerDAL,
orgMembershipDAL orgMembershipDAL
}: TAccessApprovalPolicyServiceFactoryDep) => { }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => {
const createAccessApprovalPolicy = async ({ const createAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["createAccessApprovalPolicy"] = async ({
name, name,
actor, actor,
actorId, actorId,
@ -76,35 +73,30 @@ export const accessApprovalPolicyServiceFactory = ({
projectSlug, projectSlug,
environment, environment,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals,
}: TCreateAccessApprovalPolicy) => { approvalsRequired
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
// If there is a group approver people might be added to the group later to meet the approvers quota // If there is a group approver people might be added to the group later to meet the approvers quota
const groupApprovers = approvers const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const userApprovers = approvers const userApprovers = approvers.filter((approver) => approver.type === ApproverType.User && approver.id) as {
.filter((approver) => approver.type === ApproverType.User) id: string;
.map((approver) => approver.id) sequence?: number;
.filter(Boolean) as string[]; }[];
const userApproverNames = approvers const userApproverNames = approvers.filter(
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined)) (approver) => approver.type === ApproverType.User && approver.username
.filter(Boolean) as string[]; ) as { username: string; sequence?: number }[];
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: project.id, projectId: project.id,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -116,14 +108,13 @@ export const accessApprovalPolicyServiceFactory = ({
let approverUserIds = userApprovers; let approverUserIds = userApprovers;
if (userApproverNames.length) { if (userApproverNames.length) {
const approverUsers = await userDAL.find({ const approverUsersInDB = await userDAL.find({
$in: { $in: {
username: userApproverNames username: userApproverNames.map((el) => el.username)
} }
}); });
const approverUsersInDBGroupByUsername = groupBy(approverUsersInDB, (i) => i.username);
const approverNamesFromDb = approverUsers.map((user) => user.username); const invalidUsernames = userApproverNames.filter((el) => !approverUsersInDBGroupByUsername?.[el.username]?.[0]);
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames.length) { if (invalidUsernames.length) {
throw new BadRequestError({ throw new BadRequestError({
@ -131,32 +122,13 @@ export const accessApprovalPolicyServiceFactory = ({
}); });
} }
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id)); approverUserIds = approverUserIds.concat(
} userApproverNames.map((el) => ({
id: approverUsersInDBGroupByUsername[el.username]?.[0].id,
const usersPromises: Promise< sequence: el.sequence
{ }))
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
const verifyAllApprovers = [...approverUserIds];
for (const groupId of groupApprovers) {
usersPromises.push(
groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }).then((group) => group.members)
); );
} }
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
.filter((user) => user.isPartOfGroup)
.map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers);
let groupBypassers: string[] = []; let groupBypassers: string[] = [];
let bypasserUserIds: string[] = []; let bypasserUserIds: string[] = [];
@ -195,6 +167,7 @@ export const accessApprovalPolicyServiceFactory = ({
} }
} }
const approvalsRequiredGroupByStepNumber = groupBy(approvalsRequired || [], (i) => i.stepNumber);
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create( const doc = await accessApprovalPolicyDAL.create(
{ {
@ -210,9 +183,13 @@ export const accessApprovalPolicyServiceFactory = ({
if (approverUserIds.length) { if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({ approverUserIds.map((el) => ({
approverUserId: userId, approverUserId: el.id,
policyId: doc.id policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})), })),
tx tx
); );
@ -220,9 +197,13 @@ export const accessApprovalPolicyServiceFactory = ({
if (groupApprovers) { if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({ groupApprovers.map((el) => ({
approverGroupId: groupId, approverGroupId: el.id,
policyId: doc.id policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})), })),
tx tx
); );
@ -254,31 +235,25 @@ export const accessApprovalPolicyServiceFactory = ({
return { ...accessApproval, environment: env, projectId: project.id }; return { ...accessApproval, environment: env, projectId: project.id };
}; };
const getAccessApprovalPolicyByProjectSlug = async ({ const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] =
actorId, async ({ actorId, actor, actorOrgId, actorAuthMethod, projectSlug }: TListAccessApprovalPoliciesDTO) => {
actor, const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
actorOrgId, if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
actorAuthMethod,
projectSlug
}: TListAccessApprovalPoliciesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
// Anyone in the project should be able to get the policies. // Anyone in the project should be able to get the policies.
await permissionService.getProjectPermission({ await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: project.id, projectId: project.id,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager });
});
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
return accessApprovalPolicies; return accessApprovalPolicies;
}; };
const updateAccessApprovalPolicy = async ({ const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({
policyId, policyId,
approvers, approvers,
bypassers, bypassers,
@ -290,22 +265,22 @@ export const accessApprovalPolicyServiceFactory = ({
actorAuthMethod, actorAuthMethod,
approvals, approvals,
enforcementLevel, enforcementLevel,
allowedSelfApprovals allowedSelfApprovals,
approvalsRequired
}: TUpdateAccessApprovalPolicy) => { }: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const userApprovers = approvers const userApprovers = approvers.filter((approver) => approver.type === ApproverType.User && approver.id) as {
.filter((approver) => approver.type === ApproverType.User) id: string;
.map((approver) => approver.id) sequence?: number;
.filter(Boolean) as string[]; }[];
const userApproverNames = approvers.filter(
const userApproverNames = approvers (approver) => approver.type === ApproverType.User && approver.username
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined)) ) as { username: string; sequence?: number }[];
.filter(Boolean) as string[];
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Approval policy not found" });
const currentApprovals = approvals || accessApprovalPolicy.approvals; const currentApprovals = approvals || accessApprovalPolicy.approvals;
if ( if (
groupApprovers?.length === 0 && groupApprovers?.length === 0 &&
@ -323,8 +298,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorId, actorId,
projectId: accessApprovalPolicy.projectId, projectId: accessApprovalPolicy.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
@ -401,6 +375,7 @@ export const accessApprovalPolicyServiceFactory = ({
} }
} }
const approvalsRequiredGroupByStepNumber = groupBy(approvalsRequired || [], (i) => i.stepNumber);
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.updateById( const doc = await accessApprovalPolicyDAL.updateById(
accessApprovalPolicy.id, accessApprovalPolicy.id,
@ -417,16 +392,18 @@ export const accessApprovalPolicyServiceFactory = ({
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (userApprovers.length || userApproverNames.length) { if (userApprovers.length || userApproverNames.length) {
let userApproverIds = userApprovers; let approverUserIds = userApprovers;
if (userApproverNames.length) { if (userApproverNames.length) {
const approverUsers = await userDAL.find({ const approverUsersInDB = await userDAL.find({
$in: { $in: {
username: userApproverNames username: userApproverNames.map((el) => el.username)
} }
}); });
const approverUsersInDBGroupByUsername = groupBy(approverUsersInDB, (i) => i.username);
const approverNamesFromDb = approverUsers.map((user) => user.username); const invalidUsernames = userApproverNames.filter(
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username)); (el) => !approverUsersInDBGroupByUsername?.[el.username]?.[0]
);
if (invalidUsernames.length) { if (invalidUsernames.length) {
throw new BadRequestError({ throw new BadRequestError({
@ -434,13 +411,21 @@ export const accessApprovalPolicyServiceFactory = ({
}); });
} }
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id)); approverUserIds = approverUserIds.concat(
userApproverNames.map((el) => ({
id: approverUsersInDBGroupByUsername[el.username]?.[0].id,
sequence: el.sequence
}))
);
} }
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
userApproverIds.map((userId) => ({ approverUserIds.map((el) => ({
approverUserId: userId, approverUserId: el.id,
policyId: doc.id policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})), })),
tx tx
); );
@ -448,9 +433,13 @@ export const accessApprovalPolicyServiceFactory = ({
if (groupApprovers) { if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({ groupApprovers.map((el) => ({
approverGroupId: groupId, approverGroupId: el.id,
policyId: doc.id policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})), })),
tx tx
); );
@ -478,8 +467,11 @@ export const accessApprovalPolicyServiceFactory = ({
); );
} }
await accessApprovalRequestDAL.resetReviewByPolicyId(doc.id, tx);
return doc; return doc;
}); });
return { return {
...updatedPolicy, ...updatedPolicy,
environment: accessApprovalPolicy.environment, environment: accessApprovalPolicy.environment,
@ -487,7 +479,7 @@ export const accessApprovalPolicyServiceFactory = ({
}; };
}; };
const deleteAccessApprovalPolicy = async ({ const deleteAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["deleteAccessApprovalPolicy"] = async ({
policyId, policyId,
actor, actor,
actorId, actorId,
@ -502,8 +494,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorId, actorId,
projectId: policy.projectId, projectId: policy.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
@ -536,7 +527,7 @@ export const accessApprovalPolicyServiceFactory = ({
return policy; return policy;
}; };
const getAccessPolicyCountByEnvSlug = async ({ const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@ -553,8 +544,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorId, actorId,
projectId: project.id, projectId: project.id,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
if (!membership) { if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" }); throw new ForbiddenRequestError({ message: "You are not a member of this project" });
@ -573,13 +563,13 @@ export const accessApprovalPolicyServiceFactory = ({
return { count: policies.length }; return { count: policies.length };
}; };
const getAccessApprovalPolicyById = async ({ const getAccessApprovalPolicyById: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyById"] = async ({
actorId, actorId,
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
policyId policyId
}: TGetAccessApprovalPolicyByIdDTO) => { }) => {
const [policy] = await accessApprovalPolicyDAL.find({}, { policyId }); const [policy] = await accessApprovalPolicyDAL.find({}, { policyId });
if (!policy) { if (!policy) {
@ -593,8 +583,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorId, actorId,
projectId: policy.projectId, projectId: policy.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);

View File

@ -1,7 +1,7 @@
import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod } from "@app/services/auth/auth-type"; import { ActorAuthMethod } from "@app/services/auth/auth-type";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
export type TIsApproversValid = { export type TIsApproversValid = {
userIds: string[]; userIds: string[];
@ -27,7 +27,10 @@ export type TCreateAccessApprovalPolicy = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: (
| { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
)[];
bypassers?: ( bypassers?: (
| { type: BypasserType.Group; id: string } | { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: string } | { type: BypasserType.User; id?: string; username?: string }
@ -36,12 +39,16 @@ export type TCreateAccessApprovalPolicy = {
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateAccessApprovalPolicy = { export type TUpdateAccessApprovalPolicy = {
policyId: string; policyId: string;
approvals?: number; approvals?: number;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: (
| { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
)[];
bypassers?: ( bypassers?: (
| { type: BypasserType.Group; id: string } | { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: string } | { type: BypasserType.User; id?: string; username?: string }
@ -50,6 +57,7 @@ export type TUpdateAccessApprovalPolicy = {
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteAccessApprovalPolicy = { export type TDeleteAccessApprovalPolicy = {
@ -68,3 +76,217 @@ export type TGetAccessApprovalPolicyByIdDTO = {
export type TListAccessApprovalPoliciesDTO = { export type TListAccessApprovalPoliciesDTO = {
projectSlug: string; projectSlug: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export interface TAccessApprovalPolicyServiceFactory {
getAccessPolicyCountByEnvSlug: ({
actor,
actorOrgId,
actorAuthMethod,
projectSlug,
actorId,
envSlug
}: TGetAccessPolicyCountByEnvironmentDTO) => Promise<{
count: number;
}>;
createAccessApprovalPolicy: ({
name,
actor,
actorId,
actorOrgId,
secretPath,
actorAuthMethod,
approvals,
approvers,
bypassers,
projectSlug,
environment,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
}: TCreateAccessApprovalPolicy) => Promise<{
environment: {
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
projectId: string;
slug: string;
position: number;
};
projectId: string;
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
}>;
deleteAccessApprovalPolicy: ({
policyId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TDeleteAccessApprovalPolicy) => Promise<{
approvers: {
id: string | null | undefined;
type: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
}>;
updateAccessApprovalPolicy: ({
policyId,
approvers,
bypassers,
secretPath,
name,
actorId,
actor,
actorOrgId,
actorAuthMethod,
approvals,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
}: TUpdateAccessApprovalPolicy) => Promise<{
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
}>;
getAccessApprovalPolicyByProjectSlug: ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectSlug
}: TListAccessApprovalPoliciesDTO) => Promise<
{
approvers: (
| {
id: string | null | undefined;
type: ApproverType;
name: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
| {
id: string | null | undefined;
type: ApproverType;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
)[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
bypassers: (
| {
id: string | null | undefined;
type: BypasserType;
name: string;
}
| {
id: string | null | undefined;
type: BypasserType;
}
)[];
}[]
>;
getAccessApprovalPolicyById: ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
policyId
}: TGetAccessApprovalPolicyByIdDTO) => Promise<{
approvers: (
| {
id: string | null | undefined;
type: ApproverType.User;
name: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
| {
id: string | null | undefined;
type: ApproverType.Group;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
)[];
name: string;
id: string;
createdAt: Date;
updatedAt: Date;
approvals: number;
envId: string;
enforcementLevel: string;
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
environment: {
id: string;
name: string;
slug: string;
};
projectId: string;
bypassers: (
| {
id: string | null | undefined;
type: BypasserType.User;
name: string;
}
| {
id: string | null | undefined;
type: BypasserType.Group;
}
)[];
}>;
}

View File

@ -9,195 +9,442 @@ import {
TUsers TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter, TOrmify } from "@app/lib/knex";
import { ApprovalStatus } from "./access-approval-request-types"; import { ApprovalStatus } from "./access-approval-request-types";
export type TAccessApprovalRequestDALFactory = ReturnType<typeof accessApprovalRequestDALFactory>; export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName.AccessApprovalRequest>, "findById"> {
findById: (
id: string,
tx?: Knex
) => Promise<
| {
policy: {
approvers: (
| {
userId: string | null | undefined;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
| {
userId: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
)[];
bypassers: (
| {
userId: string | null | undefined;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
}
| {
userId: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
}
)[];
id: string;
name: string;
approvals: number;
secretPath: string | null | undefined;
enforcementLevel: string;
allowedSelfApprovals: boolean;
deletedAt: Date | null | undefined;
};
projectId: string;
environment: string;
requestedByUser: {
userId: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
};
status: string;
id: string;
createdAt: Date;
updatedAt: Date;
policyId: string;
isTemporary: boolean;
requestedByUserId: string;
privilegeId?: string | null | undefined;
requestedBy?: string | null | undefined;
temporaryRange?: string | null | undefined;
permissions?: unknown;
note?: string | null | undefined;
privilegeDeletedAt?: Date | null | undefined;
reviewers: {
userId: string;
status: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
}[];
approvers: (
| {
userId: string | null | undefined;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
| {
userId: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
}
)[];
bypassers: (
| {
userId: string | null | undefined;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
}
| {
userId: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
}
)[];
}
| undefined
>;
findRequestsWithPrivilegeByPolicyIds: (policyIds: string[]) => Promise<
{
policy: {
approvers: (
| {
userId: string | null | undefined;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
| {
userId: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
)[];
bypassers: string[];
id: string;
name: string;
approvals: number;
secretPath: string | null | undefined;
enforcementLevel: string;
allowedSelfApprovals: boolean;
envId: string;
deletedAt: Date | null | undefined;
};
projectId: string;
environment: string;
environmentName: string;
requestedByUser: {
userId: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
};
privilege: {
membershipId: string;
userId: string;
projectId: string;
isTemporary: boolean;
temporaryMode: string | null | undefined;
temporaryRange: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
permissions: unknown;
} | null;
isApproved: boolean;
status: string;
id: string;
createdAt: Date;
updatedAt: Date;
policyId: string;
isTemporary: boolean;
requestedByUserId: string;
privilegeId?: string | null | undefined;
requestedBy?: string | null | undefined;
temporaryRange?: string | null | undefined;
permissions?: unknown;
note?: string | null | undefined;
privilegeDeletedAt?: Date | null | undefined;
reviewers: {
userId: string;
status: string;
}[];
approvers: (
| {
userId: string | null | undefined;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
| {
userId: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
)[];
bypassers: string[];
}[]
>;
getCount: ({ projectId }: { projectId: string; policyId?: string }) => Promise<{
pendingCount: number;
finalizedCount: number;
}>;
resetReviewByPolicyId: (policyId: string, tx?: Knex) => Promise<void>;
}
export const accessApprovalRequestDALFactory = (db: TDbClient) => { export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalRequestDALFactory => {
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest); const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => { const findRequestsWithPrivilegeByPolicyIds: TAccessApprovalRequestDALFactory["findRequestsWithPrivilegeByPolicyIds"] =
try { async (policyIds) => {
const docs = await db try {
.replicaNode()(TableName.AccessApprovalRequest) const docs = await db
.whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) .replicaNode()(TableName.AccessApprovalRequest)
.whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds)
.leftJoin( .leftJoin(
TableName.ProjectUserAdditionalPrivilege, TableName.ProjectUserAdditionalPrivilege,
`${TableName.AccessApprovalRequest}.privilegeId`, `${TableName.AccessApprovalRequest}.privilegeId`,
`${TableName.ProjectUserAdditionalPrivilege}.id` `${TableName.ProjectUserAdditionalPrivilege}.id`
) )
.leftJoin( .leftJoin(
TableName.AccessApprovalPolicy, TableName.AccessApprovalPolicy,
`${TableName.AccessApprovalRequest}.policyId`, `${TableName.AccessApprovalRequest}.policyId`,
`${TableName.AccessApprovalPolicy}.id` `${TableName.AccessApprovalPolicy}.id`
) )
.leftJoin( .leftJoin(
TableName.AccessApprovalRequestReviewer, TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`, `${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId` `${TableName.AccessApprovalRequestReviewer}.requestId`
) )
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id"
)
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyBypasser,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyBypasser}.policyId`
) )
.leftJoin( .leftJoin<TUserGroupMembership>(
TableName.UserGroupMembership, db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`, `${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`,
`${TableName.UserGroupMembership}.groupId` `bypasserUserGroupMembership.groupId`
) )
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin( .join<TUsers>(
TableName.AccessApprovalPolicyBypasser, db(TableName.Users).as("requestedByUser"),
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalRequest}.requestedByUserId`,
`${TableName.AccessApprovalPolicyBypasser}.policyId` `requestedByUser.id`
) )
.leftJoin<TUserGroupMembership>(
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId`
)
.join<TUsers>( .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
db(TableName.Users).as("requestedByUser"),
`${TableName.AccessApprovalRequest}.requestedByUserId`,
`requestedByUser.id`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
)
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
.select(db.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
.select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"))
.select(
db.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
db.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
db.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
db.ref("username").withSchema(TableName.Users).as("approverGroupUsername")
)
.select(
db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
)
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(
.select( db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"),
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), )
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
)
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) // TODO: ADD SUPPORT FOR GROUPS!!!!
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId")) .select(
db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"),
.select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser)) db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"),
.select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId")) db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"),
.select( db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"),
db.ref("projectId").withSchema(TableName.Environment), db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
db.ref("name").withSchema(TableName.Environment).as("envName") db
) .ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeTemporaryAccessEndTime"),
.select( db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions")
db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"), )
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus") .orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc");
)
// TODO: ADD SUPPORT FOR GROUPS!!!! const formattedDocs = sqlNestRelationships({
.select( data: docs,
db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"), key: "id",
db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"), parentMapper: (doc) => ({
db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), ...AccessApprovalRequestsSchema.parse(doc),
db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"), projectId: doc.projectId,
environment: doc.envSlug,
environmentName: doc.envName,
policy: {
id: doc.policyId,
name: doc.policyName,
approvals: doc.policyApprovals,
secretPath: doc.policySecretPath,
enforcementLevel: doc.policyEnforcementLevel,
allowedSelfApprovals: doc.policyAllowedSelfApprovals,
envId: doc.policyEnvId,
deletedAt: doc.policyDeletedAt
},
requestedByUser: {
userId: doc.requestedByUserId,
email: doc.requestedByUserEmail,
firstName: doc.requestedByUserFirstName,
lastName: doc.requestedByUserLastName,
username: doc.requestedByUserUsername
},
privilege: doc.privilegeId
? {
membershipId: doc.privilegeMembershipId,
userId: doc.privilegeUserId,
projectId: doc.projectId,
isTemporary: doc.privilegeIsTemporary,
temporaryMode: doc.privilegeTemporaryMode,
temporaryRange: doc.privilegeTemporaryRange,
temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime,
temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime,
permissions: doc.privilegePermissions
}
: null,
isApproved: doc.status === ApprovalStatus.APPROVED
}),
childrenMapper: [
{
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
},
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({
userId: approverUserId,
sequence: approverSequence,
approvalsRequired,
email: approverEmail,
username: approverUsername
})
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({
approverGroupUserId,
approverSequence,
approvalsRequired,
approverGroupEmail,
approverGroupUsername
}) => ({
userId: approverGroupUserId,
sequence: approverSequence,
approvalsRequired,
email: approverGroupEmail,
username: approverGroupUsername
})
},
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
{
key: "bypasserGroupUserId",
label: "bypassers" as const,
mapper: ({ bypasserGroupUserId }) => bypasserGroupUserId
}
]
});
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"), if (!formattedDocs) return [];
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), return formattedDocs.map((doc) => ({
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), ...doc,
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeTemporaryAccessEndTime"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions")
)
.orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc");
const formattedDocs = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc),
projectId: doc.projectId,
environment: doc.envSlug,
environmentName: doc.envName,
policy: { policy: {
id: doc.policyId, ...doc.policy,
name: doc.policyName, approvers: doc.approvers.filter((el) => el.userId).sort((a, b) => (a.sequence || 0) - (b.sequence || 0)),
approvals: doc.policyApprovals, bypassers: doc.bypassers
secretPath: doc.policySecretPath,
enforcementLevel: doc.policyEnforcementLevel,
allowedSelfApprovals: doc.policyAllowedSelfApprovals,
envId: doc.policyEnvId,
deletedAt: doc.policyDeletedAt
},
requestedByUser: {
userId: doc.requestedByUserId,
email: doc.requestedByUserEmail,
firstName: doc.requestedByUserFirstName,
lastName: doc.requestedByUserLastName,
username: doc.requestedByUserUsername
},
privilege: doc.privilegeId
? {
membershipId: doc.privilegeMembershipId,
userId: doc.privilegeUserId,
projectId: doc.projectId,
isTemporary: doc.privilegeIsTemporary,
temporaryMode: doc.privilegeTemporaryMode,
temporaryRange: doc.privilegeTemporaryRange,
temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime,
temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime,
permissions: doc.privilegePermissions
}
: null,
isApproved: !!doc.policyDeletedAt || !!doc.privilegeId || doc.status !== ApprovalStatus.PENDING
}),
childrenMapper: [
{
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
},
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => approverGroupUserId
},
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
{
key: "bypasserGroupUserId",
label: "bypassers" as const,
mapper: ({ bypasserGroupUserId }) => bypasserGroupUserId
} }
] }));
}); } catch (error) {
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
if (!formattedDocs) return []; }
};
return formattedDocs.map((doc) => ({
...doc,
policy: { ...doc.policy, approvers: doc.approvers, bypassers: doc.bypassers }
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
}
};
const findQuery = (filter: TFindFilter<TAccessApprovalRequests>, tx: Knex) => const findQuery = (filter: TFindFilter<TAccessApprovalRequests>, tx: Knex) =>
tx(TableName.AccessApprovalRequest) tx(TableName.AccessApprovalRequest)
@ -272,6 +519,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"),
tx.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership), tx.ref("userId").withSchema(TableName.UserGroupMembership),
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"), tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
@ -318,7 +567,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt") tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
); );
const findById = async (id: string, tx?: Knex) => { const findById: TAccessApprovalRequestDALFactory["findById"] = async (id, tx) => {
try { try {
const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode()); const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode());
const docs = await sql; const docs = await sql;
@ -367,13 +616,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approverEmail: email, approverEmail: email,
approverUsername: username, approverUsername: username,
approverLastName: lastName, approverLastName: lastName,
approverFirstName: firstName approverFirstName: firstName,
approverSequence,
approvalsRequired
}) => ({ }) => ({
userId: approverUserId, userId: approverUserId,
email, email,
firstName, firstName,
lastName, lastName,
username username,
sequence: approverSequence,
approvalsRequired
}) })
}, },
{ {
@ -384,13 +637,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approverGroupEmail: email, approverGroupEmail: email,
approverGroupUsername: username, approverGroupUsername: username,
approverGroupLastName: lastName, approverGroupLastName: lastName,
approverFirstName: firstName approverFirstName: firstName,
approverSequence,
approvalsRequired
}) => ({ }) => ({
userId, userId,
email, email,
firstName, firstName,
lastName, lastName,
username username,
sequence: approverSequence,
approvalsRequired
}) })
}, },
{ {
@ -434,7 +691,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
...formattedDoc[0], ...formattedDoc[0],
policy: { policy: {
...formattedDoc[0].policy, ...formattedDoc[0].policy,
approvers: formattedDoc[0].approvers, approvers: formattedDoc[0].approvers
.filter((el) => el.userId)
.sort((a, b) => (a.sequence || 0) - (b.sequence || 0)),
bypassers: formattedDoc[0].bypassers bypassers: formattedDoc[0].bypassers
} }
}; };
@ -443,7 +702,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
} }
}; };
const getCount = async ({ projectId }: { projectId: string }) => { const getCount: TAccessApprovalRequestDALFactory["getCount"] = async ({ projectId, policyId }) => {
try { try {
const accessRequests = await db const accessRequests = await db
.replicaNode()(TableName.AccessApprovalRequest) .replicaNode()(TableName.AccessApprovalRequest)
@ -464,18 +723,21 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalRequest}.id`, `${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId` `${TableName.AccessApprovalRequestReviewer}.requestId`
) )
.where(`${TableName.Environment}.projectId`, projectId) .where(`${TableName.Environment}.projectId`, projectId)
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null) .where((qb) => {
if (policyId) void qb.where(`${TableName.AccessApprovalPolicy}.id`, policyId);
})
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) .select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId")); .select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"))
.select(db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"));
const formattedRequests = sqlNestRelationships({ const formattedRequests = sqlNestRelationships({
data: accessRequests, data: accessRequests,
key: "id", key: "id",
parentMapper: (doc) => ({ parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc) ...AccessApprovalRequestsSchema.parse(doc),
isPolicyDeleted: Boolean(doc.policyDeletedAt)
}), }),
childrenMapper: [ childrenMapper: [
{ {
@ -492,15 +754,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
(req) => (req) =>
!req.privilegeId && !req.privilegeId &&
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
req.status === ApprovalStatus.PENDING req.status === ApprovalStatus.PENDING &&
!req.isPolicyDeleted
); );
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required // an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
const finalizedApprovals = formattedRequests.filter( const finalizedApprovals = formattedRequests.filter(
(req) => (req) =>
req.privilegeId || req.privilegeId ||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
req.status !== ApprovalStatus.PENDING req.status !== ApprovalStatus.PENDING ||
req.isPolicyDeleted
); );
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length }; return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
@ -509,5 +773,27 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
} }
}; };
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount }; const resetReviewByPolicyId: TAccessApprovalRequestDALFactory["resetReviewByPolicyId"] = async (policyId, tx) => {
try {
await (tx || db)(TableName.AccessApprovalRequestReviewer)
.leftJoin(
TableName.AccessApprovalRequest,
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.where(`${TableName.AccessApprovalRequest}.status` as "status", ApprovalStatus.PENDING)
.where(`${TableName.AccessApprovalRequest}.policyId` as "policyId", policyId)
.del();
} catch (error) {
throw new DatabaseError({ error, name: "ResetReviewByPolicyId" });
}
};
return {
...accessApprovalRequestOrm,
findById,
findRequestsWithPrivilegeByPolicyIds,
getCount,
resetReviewByPolicyId
};
}; };

View File

@ -1,10 +1,10 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { ormify, TOrmify } from "@app/lib/knex";
export type TAccessApprovalRequestReviewerDALFactory = ReturnType<typeof accessApprovalRequestReviewerDALFactory>; export type TAccessApprovalRequestReviewerDALFactory = TOrmify<TableName.AccessApprovalRequestReviewer>;
export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => { export const accessApprovalRequestReviewerDALFactory = (db: TDbClient): TAccessApprovalRequestReviewerDALFactory => {
const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer); const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer);
return secretApprovalRequestReviewerOrm; return secretApprovalRequestReviewerOrm;
}; };

View File

@ -1,9 +1,10 @@
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import msFn from "ms"; import msFn from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
@ -22,19 +23,13 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { TGroupDALFactory } from "../group/group-dal"; import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
import { verifyRequestedPermissions } from "./access-approval-request-fns"; import { verifyRequestedPermissions } from "./access-approval-request-fns";
import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal";
import { import { ApprovalStatus, TAccessApprovalRequestServiceFactory } from "./access-approval-request-types";
ApprovalStatus,
TCreateAccessApprovalRequestDTO,
TGetAccessRequestCountDTO,
TListApprovalRequestsDTO,
TReviewAccessRequestDTO
} from "./access-approval-request-types";
type TSecretApprovalRequestServiceFactoryDep = { type TSecretApprovalRequestServiceFactoryDep = {
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">; additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">;
@ -74,8 +69,6 @@ type TSecretApprovalRequestServiceFactoryDep = {
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">; projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
}; };
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
export const accessApprovalRequestServiceFactory = ({ export const accessApprovalRequestServiceFactory = ({
groupDAL, groupDAL,
projectDAL, projectDAL,
@ -92,8 +85,8 @@ export const accessApprovalRequestServiceFactory = ({
microsoftTeamsService, microsoftTeamsService,
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
projectSlackConfigDAL projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => {
const createAccessApprovalRequest = async ({ const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({
isTemporary, isTemporary,
temporaryRange, temporaryRange,
actorId, actorId,
@ -103,7 +96,7 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod, actorAuthMethod,
projectSlug, projectSlug,
note note
}: TCreateAccessApprovalRequestDTO) => { }) => {
const cfg = getConfig(); const cfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -114,8 +107,7 @@ export const accessApprovalRequestServiceFactory = ({
actorId, actorId,
projectId: project.id, projectId: project.id,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
if (!membership) { if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" }); throw new ForbiddenRequestError({ message: "You are not a member of this project" });
@ -224,7 +216,7 @@ export const accessApprovalRequestServiceFactory = ({
); );
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`; const approvalUrl = `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval`;
await triggerWorkflowIntegrationNotification({ await triggerWorkflowIntegrationNotification({
input: { input: {
@ -280,15 +272,15 @@ export const accessApprovalRequestServiceFactory = ({
return { request: approval }; return { request: approval };
}; };
const listApprovalRequests = async ({ const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
projectSlug, projectSlug,
authorProjectMembershipId, authorUserId,
envSlug, envSlug,
actor, actor,
actorOrgId, actorOrgId,
actorId, actorId,
actorAuthMethod actorAuthMethod
}: TListApprovalRequestsDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -297,8 +289,7 @@ export const accessApprovalRequestServiceFactory = ({
actorId, actorId,
projectId: project.id, projectId: project.id,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
if (!membership) { if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" }); throw new ForbiddenRequestError({ message: "You are not a member of this project" });
@ -307,8 +298,8 @@ export const accessApprovalRequestServiceFactory = ({
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
if (authorProjectMembershipId) { if (authorUserId) {
requests = requests.filter((request) => request.requestedByUserId === actorId); requests = requests.filter((request) => request.requestedByUserId === authorUserId);
} }
if (envSlug) { if (envSlug) {
@ -318,7 +309,7 @@ export const accessApprovalRequestServiceFactory = ({
return { requests }; return { requests };
}; };
const reviewAccessRequest = async ({ const reviewAccessRequest: TAccessApprovalRequestServiceFactory["reviewAccessRequest"] = async ({
requestId, requestId,
actor, actor,
status, status,
@ -326,7 +317,7 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
bypassReason bypassReason
}: TReviewAccessRequestDTO) => { }) => {
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
if (!accessApprovalRequest) { if (!accessApprovalRequest) {
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` }); throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
@ -344,8 +335,7 @@ export const accessApprovalRequestServiceFactory = ({
actorId, actorId,
projectId: accessApprovalRequest.projectId, projectId: accessApprovalRequest.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
if (!membership) { if (!membership) {
@ -357,8 +347,13 @@ export const accessApprovalRequestServiceFactory = ({
const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId); const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId);
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass); const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
const isApprover = policy.approvers.find((approver) => approver.userId === actorId); // Calculate break glass attempt before sequence checks
const isBreakGlassApprovalAttempt =
policy.enforcementLevel === EnforcementLevel.Soft &&
actorId === accessApprovalRequest.requestedByUserId &&
status === ApprovalStatus.APPROVED;
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
// If user is (not an approver OR cant self approve) AND can't bypass policy // If user is (not an approver OR cant self approve) AND can't bypass policy
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) { if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
throw new BadRequestError({ throw new BadRequestError({
@ -380,16 +375,51 @@ export const accessApprovalRequestServiceFactory = ({
} }
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { if (accessApprovalRequest.status !== ApprovalStatus.PENDING) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); throw new BadRequestError({ message: "The request has been closed" });
}
const reviewsGroupById = groupBy(
existingReviews.filter((review) => review.status === ApprovalStatus.APPROVED),
(i) => i.reviewerUserId
);
const approvedSequences = policy.approvers.reduce(
(acc, curr) => {
const hasApproved = reviewsGroupById?.[curr.userId as string]?.[0];
if (acc?.[acc.length - 1]?.step === curr.sequence) {
if (hasApproved) {
acc[acc.length - 1].approvals += 1;
}
return acc;
}
acc.push({
step: curr.sequence || 1,
approvals: hasApproved ? 1 : 0,
requiredApprovals: curr.approvalsRequired || 1
});
return acc;
},
[] as { step: number; approvals: number; requiredApprovals: number }[]
);
const presentSequence = approvedSequences.find((el) => el.approvals < el.requiredApprovals) || {
step: 1,
approvals: 0,
requiredApprovals: 1
};
if (presentSequence) {
const isApproverOfTheSequence = policy.approvers.find(
(el) => el.sequence === presentSequence.step && el.userId === actorId
);
// Only throw if actor is not the approver and not bypassing
if (!isApproverOfTheSequence && !isBreakGlassApprovalAttempt) {
throw new BadRequestError({ message: "You are not a reviewer in this step" });
}
} }
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => { const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
const isBreakGlassApprovalAttempt =
policy.enforcementLevel === EnforcementLevel.Soft &&
actorId === accessApprovalRequest.requestedByUserId &&
status === ApprovalStatus.APPROVED;
let reviewForThisActorProcessing: { let reviewForThisActorProcessing: {
id: string; id: string;
requestId: string; requestId: string;
@ -426,11 +456,14 @@ export const accessApprovalRequestServiceFactory = ({
); );
} }
const otherReviews = existingReviews.filter((er) => er.reviewerUserId !== actorId); if (status === ApprovalStatus.REJECTED) {
const allUniqueReviews = [...otherReviews, reviewForThisActorProcessing]; await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { status: ApprovalStatus.REJECTED }, tx);
return reviewForThisActorProcessing;
}
const approvedReviews = allUniqueReviews.filter((r) => r.status === ApprovalStatus.APPROVED); const meetsStandardApprovalThreshold =
const meetsStandardApprovalThreshold = approvedReviews.length >= policy.approvals; (presentSequence?.approvals || 0) + 1 >= presentSequence.requiredApprovals &&
approvedSequences.at(-1)?.step === presentSequence?.step;
if ( if (
reviewForThisActorProcessing.status === ApprovalStatus.APPROVED && reviewForThisActorProcessing.status === ApprovalStatus.APPROVED &&
@ -512,7 +545,7 @@ export const accessApprovalRequestServiceFactory = ({
bypassReason: bypassReason || "No reason provided", bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/", secretPath: policy.secretPath || "/",
environment, environment,
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`, approvalUrl: `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval`,
requestType: "access" requestType: "access"
}, },
template: SmtpTemplates.AccessSecretRequestBypassed template: SmtpTemplates.AccessSecretRequestBypassed
@ -527,7 +560,14 @@ export const accessApprovalRequestServiceFactory = ({
return reviewStatus; return reviewStatus;
}; };
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => { const getCount: TAccessApprovalRequestServiceFactory["getCount"] = async ({
projectSlug,
policyId,
actor,
actorAuthMethod,
actorId,
actorOrgId
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -536,14 +576,13 @@ export const accessApprovalRequestServiceFactory = ({
actorId, actorId,
projectId: project.id, projectId: project.id,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
if (!membership) { if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" }); throw new ForbiddenRequestError({ message: "You are not a member of this project" });
} }
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id }); const count = await accessApprovalRequestDAL.getCount({ projectId: project.id, policyId });
return { count }; return { count };
}; };

View File

@ -12,6 +12,7 @@ export type TVerifyPermission = {
export type TGetAccessRequestCountDTO = { export type TGetAccessRequestCountDTO = {
projectSlug: string; projectSlug: string;
policyId?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TReviewAccessRequestDTO = { export type TReviewAccessRequestDTO = {
@ -31,6 +32,127 @@ export type TCreateAccessApprovalRequestDTO = {
export type TListApprovalRequestsDTO = { export type TListApprovalRequestsDTO = {
projectSlug: string; projectSlug: string;
authorProjectMembershipId?: string; authorUserId?: string;
envSlug?: string; envSlug?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export interface TAccessApprovalRequestServiceFactory {
createAccessApprovalRequest: (arg: TCreateAccessApprovalRequestDTO) => Promise<{
request: {
status: string;
id: string;
createdAt: Date;
updatedAt: Date;
policyId: string;
isTemporary: boolean;
requestedByUserId: string;
privilegeId?: string | null | undefined;
requestedBy?: string | null | undefined;
temporaryRange?: string | null | undefined;
permissions?: unknown;
note?: string | null | undefined;
privilegeDeletedAt?: Date | null | undefined;
};
}>;
listApprovalRequests: (arg: TListApprovalRequestsDTO) => Promise<{
requests: {
policy: {
approvers: (
| {
userId: string | null | undefined;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
| {
userId: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
)[];
bypassers: string[];
id: string;
name: string;
approvals: number;
secretPath: string | null | undefined;
enforcementLevel: string;
allowedSelfApprovals: boolean;
envId: string;
deletedAt: Date | null | undefined;
};
projectId: string;
environment: string;
environmentName: string;
requestedByUser: {
userId: string;
email: string | null | undefined;
firstName: string | null | undefined;
lastName: string | null | undefined;
username: string;
};
privilege: {
membershipId: string;
userId: string;
projectId: string;
isTemporary: boolean;
temporaryMode: string | null | undefined;
temporaryRange: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
permissions: unknown;
} | null;
isApproved: boolean;
status: string;
id: string;
createdAt: Date;
updatedAt: Date;
policyId: string;
isTemporary: boolean;
requestedByUserId: string;
privilegeId?: string | null | undefined;
requestedBy?: string | null | undefined;
temporaryRange?: string | null | undefined;
permissions?: unknown;
note?: string | null | undefined;
privilegeDeletedAt?: Date | null | undefined;
reviewers: {
userId: string;
status: string;
}[];
approvers: (
| {
userId: string | null | undefined;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
| {
userId: string;
sequence: number | null | undefined;
approvalsRequired: number | null | undefined;
email: string | null | undefined;
username: string;
}
)[];
bypassers: string[];
}[];
}>;
reviewAccessRequest: (arg: TReviewAccessRequestDTO) => Promise<{
id: string;
requestId: string;
reviewerUserId: string;
status: string;
createdAt: Date;
updatedAt: Date;
}>;
getCount: (arg: TGetAccessRequestCountDTO) => Promise<{
count: {
pendingCount: number;
finalizedCount: number;
};
}>;
}

View File

@ -1,35 +1,35 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { import {
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionSub ProjectPermissionSub
} from "../permission/project-permission"; } from "../permission/project-permission";
import { TAssumeProjectPrivilegeDTO } from "./assume-privilege-types"; import { TAssumePrivilegeServiceFactory } from "./assume-privilege-types";
type TAssumePrivilegeServiceFactoryDep = { type TAssumePrivilegeServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
}; };
export type TAssumePrivilegeServiceFactory = ReturnType<typeof assumePrivilegeServiceFactory>; export const assumePrivilegeServiceFactory = ({
projectDAL,
export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: TAssumePrivilegeServiceFactoryDep) => { permissionService
const assumeProjectPrivileges = async ({ }: TAssumePrivilegeServiceFactoryDep): TAssumePrivilegeServiceFactory => {
const assumeProjectPrivileges: TAssumePrivilegeServiceFactory["assumeProjectPrivileges"] = async ({
targetActorType, targetActorType,
targetActorId, targetActorId,
projectId, projectId,
actorPermissionDetails, actorPermissionDetails,
tokenVersionId tokenVersionId
}: TAssumeProjectPrivilegeDTO) => { }) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@ -37,8 +37,7 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }:
actorId: actorPermissionDetails.id, actorId: actorPermissionDetails.id,
projectId, projectId,
actorAuthMethod: actorPermissionDetails.authMethod, actorAuthMethod: actorPermissionDetails.authMethod,
actorOrgId: actorPermissionDetails.orgId, actorOrgId: actorPermissionDetails.orgId
actionProjectType: ActionProjectType.Any
}); });
if (targetActorType === ActorType.USER) { if (targetActorType === ActorType.USER) {
@ -59,8 +58,7 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }:
actorId: targetActorId, actorId: targetActorId,
projectId, projectId,
actorAuthMethod: actorPermissionDetails.authMethod, actorAuthMethod: actorPermissionDetails.authMethod,
actorOrgId: actorPermissionDetails.orgId, actorOrgId: actorPermissionDetails.orgId
actionProjectType: ActionProjectType.Any
}); });
const appCfg = getConfig(); const appCfg = getConfig();
@ -79,7 +77,10 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }:
return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken }; return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken };
}; };
const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => { const verifyAssumePrivilegeToken: TAssumePrivilegeServiceFactory["verifyAssumePrivilegeToken"] = (
token,
tokenVersionId
) => {
const appCfg = getConfig(); const appCfg = getConfig();
const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as { const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as {
tokenVersionId: string; tokenVersionId: string;

View File

@ -8,3 +8,28 @@ export type TAssumeProjectPrivilegeDTO = {
tokenVersionId: string; tokenVersionId: string;
actorPermissionDetails: OrgServiceActor; actorPermissionDetails: OrgServiceActor;
}; };
export interface TAssumePrivilegeServiceFactory {
assumeProjectPrivileges: ({
targetActorType,
targetActorId,
projectId,
actorPermissionDetails,
tokenVersionId
}: TAssumeProjectPrivilegeDTO) => Promise<{
actorType: ActorType.USER | ActorType.IDENTITY;
actorId: string;
projectId: string;
assumePrivilegesToken: string;
}>;
verifyAssumePrivilegeToken: (
token: string,
tokenVersionId: string
) => {
tokenVersionId: string;
projectId: string;
requesterId: string;
actorType: ActorType;
actorId: string;
};
}

View File

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

View File

@ -0,0 +1,21 @@
export function providerSpecificPayload(url: string) {
const { hostname } = new URL(url);
const payload: Record<string, string> = {};
switch (hostname) {
case "http-intake.logs.datadoghq.com":
case "http-intake.logs.us3.datadoghq.com":
case "http-intake.logs.us5.datadoghq.com":
case "http-intake.logs.datadoghq.eu":
case "http-intake.logs.ap1.datadoghq.com":
case "http-intake.logs.ddog-gov.com":
payload.ddsource = "infisical";
payload.service = "audit-logs";
break;
default:
break;
}
return payload;
}

View File

@ -11,16 +11,10 @@ import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue"; import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal"; import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
import { import { providerSpecificPayload } from "./audit-log-stream-fns";
LogStreamHeaders, import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types";
TCreateAuditLogStreamDTO,
TDeleteAuditLogStreamDTO,
TGetDetailsAuditLogStreamDTO,
TListAuditLogStreamDTO,
TUpdateAuditLogStreamDTO
} from "./audit-log-stream-types";
type TAuditLogStreamServiceFactoryDep = { type TAuditLogStreamServiceFactoryDep = {
auditLogStreamDAL: TAuditLogStreamDALFactory; auditLogStreamDAL: TAuditLogStreamDALFactory;
@ -28,21 +22,19 @@ type TAuditLogStreamServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TAuditLogStreamServiceFactory = ReturnType<typeof auditLogStreamServiceFactory>;
export const auditLogStreamServiceFactory = ({ export const auditLogStreamServiceFactory = ({
auditLogStreamDAL, auditLogStreamDAL,
permissionService, permissionService,
licenseService licenseService
}: TAuditLogStreamServiceFactoryDep) => { }: TAuditLogStreamServiceFactoryDep): TAuditLogStreamServiceFactory => {
const create = async ({ const create: TAuditLogStreamServiceFactory["create"] = async ({
url, url,
actor, actor,
headers = [], headers = [],
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
}: TCreateAuditLogStreamDTO) => { }) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@ -78,10 +70,11 @@ export const auditLogStreamServiceFactory = ({
headers.forEach(({ key, value }) => { headers.forEach(({ key, value }) => {
streamHeaders[key] = value; streamHeaders[key] = value;
}); });
await request await request
.post( .post(
url, url,
{ ping: "ok" }, { ...providerSpecificPayload(url), ping: "ok" },
{ {
headers: streamHeaders, headers: streamHeaders,
// request timeout // request timeout
@ -110,7 +103,7 @@ export const auditLogStreamServiceFactory = ({
return logStream; return logStream;
}; };
const updateById = async ({ const updateById: TAuditLogStreamServiceFactory["updateById"] = async ({
id, id,
url, url,
actor, actor,
@ -118,7 +111,7 @@ export const auditLogStreamServiceFactory = ({
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
}: TUpdateAuditLogStreamDTO) => { }) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@ -146,7 +139,7 @@ export const auditLogStreamServiceFactory = ({
await request await request
.post( .post(
url || logStream.url, url || logStream.url,
{ ping: "ok" }, { ...providerSpecificPayload(url || logStream.url), ping: "ok" },
{ {
headers: streamHeaders, headers: streamHeaders,
// request timeout // request timeout
@ -175,7 +168,13 @@ export const auditLogStreamServiceFactory = ({
return updatedLogStream; return updatedLogStream;
}; };
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => { const deleteById: TAuditLogStreamServiceFactory["deleteById"] = async ({
id,
actor,
actorId,
actorOrgId,
actorAuthMethod
}) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
const logStream = await auditLogStreamDAL.findById(id); const logStream = await auditLogStreamDAL.findById(id);
@ -189,7 +188,13 @@ export const auditLogStreamServiceFactory = ({
return deletedLogStream; return deletedLogStream;
}; };
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => { const getById: TAuditLogStreamServiceFactory["getById"] = async ({
id,
actor,
actorId,
actorOrgId,
actorAuthMethod
}) => {
const logStream = await auditLogStreamDAL.findById(id); const logStream = await auditLogStreamDAL.findById(id);
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
@ -212,7 +217,7 @@ export const auditLogStreamServiceFactory = ({
return { ...logStream, headers }; return { ...logStream, headers };
}; };
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => { const list: TAuditLogStreamServiceFactory["list"] = async ({ actor, actorId, actorOrgId, actorAuthMethod }) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,

View File

@ -1,3 +1,4 @@
import { TAuditLogStreams } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
export type LogStreamHeaders = { export type LogStreamHeaders = {
@ -25,3 +26,23 @@ export type TListAuditLogStreamDTO = Omit<TOrgPermission, "orgId">;
export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & { export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
id: string; id: string;
}; };
export type TAuditLogStreamServiceFactory = {
create: (arg: TCreateAuditLogStreamDTO) => Promise<TAuditLogStreams>;
updateById: (arg: TUpdateAuditLogStreamDTO) => Promise<TAuditLogStreams>;
deleteById: (arg: TDeleteAuditLogStreamDTO) => Promise<TAuditLogStreams>;
getById: (arg: TGetDetailsAuditLogStreamDTO) => Promise<{
headers: LogStreamHeaders[] | undefined;
orgId: string;
url: string;
id: string;
createdAt: Date;
updatedAt: Date;
encryptedHeadersCiphertext?: string | null | undefined;
encryptedHeadersIV?: string | null | undefined;
encryptedHeadersTag?: string | null | undefined;
encryptedHeadersAlgorithm?: string | null | undefined;
encryptedHeadersKeyEncoding?: string | null | undefined;
}>;
list: (arg: TListAuditLogStreamDTO) => Promise<TAuditLogStreams[]>;
};

View File

@ -2,25 +2,38 @@
import knex from "knex"; import knex from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName, TAuditLogs } from "@app/db/schemas";
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors"; import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { EventType, filterableSecretEvents } from "./audit-log-types"; import { EventType, filterableSecretEvents } from "./audit-log-types";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>; export interface TAuditLogDALFactory extends Omit<TOrmify<TableName.AuditLog>, "find"> {
pruneAuditLog: (tx?: knex.Knex) => Promise<void>;
find: (
arg: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string | undefined;
actorType?: ActorType | undefined;
secretPath?: string | undefined;
secretKey?: string | undefined;
eventType?: EventType[] | undefined;
eventMetadata?: Record<string, string> | undefined;
},
tx?: knex.Knex
) => Promise<TAuditLogs[]>;
}
type TFindQuery = { type TFindQuery = {
actor?: string; actor?: string;
projectId?: string; projectId?: string;
environment?: string; environment?: string;
orgId?: string; orgId: string;
eventType?: string; eventType?: string;
startDate?: string; startDate: string;
endDate?: string; endDate: string;
userAgentType?: string; userAgentType?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
@ -29,7 +42,7 @@ type TFindQuery = {
export const auditLogDALFactory = (db: TDbClient) => { export const auditLogDALFactory = (db: TDbClient) => {
const auditLogOrm = ormify(db, TableName.AuditLog); const auditLogOrm = ormify(db, TableName.AuditLog);
const find = async ( const find: TAuditLogDALFactory["find"] = async (
{ {
orgId, orgId,
projectId, projectId,
@ -45,28 +58,18 @@ export const auditLogDALFactory = (db: TDbClient) => {
secretKey, secretKey,
eventType, eventType,
eventMetadata eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
secretPath?: string;
secretKey?: string;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
}, },
tx?: knex.Knex tx
) => { ) => {
if (!orgId && !projectId) {
throw new Error("Either orgId or projectId must be provided");
}
try { try {
// Find statements // Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog) const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
.where(`${TableName.AuditLog}.orgId`, orgId)
.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate])
.andWhereRaw(`"${TableName.AuditLog}"."createdAt" < ?::timestamptz`, [endDate])
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
.where(function () { .where(function () {
if (orgId) { if (projectId) {
void this.where(`${TableName.AuditLog}.orgId`, orgId);
} else if (projectId) {
void this.where(`${TableName.AuditLog}.projectId`, projectId); void this.where(`${TableName.AuditLog}.projectId`, projectId);
} }
}); });
@ -129,14 +132,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
void sqlQuery.whereIn("eventType", eventType); void sqlQuery.whereIn("eventType", eventType);
} }
// Filter by date range
if (startDate) {
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
}
if (endDate) {
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
}
// we timeout long running queries to prevent DB resource issues (2 minutes) // we timeout long running queries to prevent DB resource issues (2 minutes)
const docs = await sqlQuery.timeout(1000 * 120); const docs = await sqlQuery.timeout(1000 * 120);
@ -154,7 +149,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
}; };
// delete all audit log that have expired // delete all audit log that have expired
const pruneAuditLog = async (tx?: knex.Knex) => { const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000; const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3; const MAX_RETRY_ON_FAILURE = 3;
@ -168,6 +163,8 @@ export const auditLogDALFactory = (db: TDbClient) => {
try { try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog) const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
.where("expiresAt", "<", today) .where("expiresAt", "<", today)
.where("createdAt", "<", today) // to use audit log partition
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
.select("id") .select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE); .limit(AUDIT_LOG_PRUNE_BATCH_SIZE);

View File

@ -1,13 +1,15 @@
import { RawAxiosRequestHeaders } from "axios"; import { AxiosError, RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal"; import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal";
import { providerSpecificPayload } from "../audit-log-stream/audit-log-stream-fns";
import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types"; import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TAuditLogDALFactory } from "./audit-log-dal"; import { TAuditLogDALFactory } from "./audit-log-dal";
@ -21,7 +23,9 @@ type TAuditLogQueueServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TAuditLogQueueServiceFactory = Awaited<ReturnType<typeof auditLogQueueServiceFactory>>; export type TAuditLogQueueServiceFactory = {
pushToLog: (data: TCreateAuditLogDTO) => Promise<void>;
};
// keep this timeout 5s it must be fast because else the queue will take time to finish // keep this timeout 5s it must be fast because else the queue will take time to finish
// audit log is a crowded queue thus needs to be fast // audit log is a crowded queue thus needs to be fast
@ -33,7 +37,7 @@ export const auditLogQueueServiceFactory = async ({
projectDAL, projectDAL,
licenseService, licenseService,
auditLogStreamDAL auditLogStreamDAL
}: TAuditLogQueueServiceFactoryDep) => { }: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
const appCfg = getConfig(); const appCfg = getConfig();
const pushToLog = async (data: TCreateAuditLogDTO) => { const pushToLog = async (data: TCreateAuditLogDTO) => {
@ -126,13 +130,25 @@ export const auditLogQueueServiceFactory = async ({
headers[key] = value; headers[key] = value;
}); });
return request.post(url, auditLog, { try {
headers, const response = await request.post(
// request timeout url,
timeout: AUDIT_LOG_STREAM_TIMEOUT, { ...providerSpecificPayload(url), ...auditLog },
// connection timeout {
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) 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;
}
} }
) )
); );
@ -216,13 +232,25 @@ export const auditLogQueueServiceFactory = async ({
headers[key] = value; headers[key] = value;
}); });
return request.post(url, auditLog, { try {
headers, const response = await request.post(
// request timeout url,
timeout: AUDIT_LOG_STREAM_TIMEOUT, { ...providerSpecificPayload(url), ...auditLog },
// connection timeout {
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) 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;
}
} }
) )
); );

View File

@ -1,17 +1,16 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context"; import { requestContext } from "@fastify/request-context";
import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TAuditLogDALFactory } from "./audit-log-dal"; import { TAuditLogDALFactory } from "./audit-log-dal";
import { TAuditLogQueueServiceFactory } from "./audit-log-queue"; import { TAuditLogQueueServiceFactory } from "./audit-log-queue";
import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-log-types"; import { EventType, TAuditLogServiceFactory } from "./audit-log-types";
type TAuditLogServiceFactoryDep = { type TAuditLogServiceFactoryDep = {
auditLogDAL: TAuditLogDALFactory; auditLogDAL: TAuditLogDALFactory;
@ -19,14 +18,18 @@ type TAuditLogServiceFactoryDep = {
auditLogQueue: TAuditLogQueueServiceFactory; auditLogQueue: TAuditLogQueueServiceFactory;
}; };
export type TAuditLogServiceFactory = ReturnType<typeof auditLogServiceFactory>;
export const auditLogServiceFactory = ({ export const auditLogServiceFactory = ({
auditLogDAL, auditLogDAL,
auditLogQueue, auditLogQueue,
permissionService permissionService
}: TAuditLogServiceFactoryDep) => { }: TAuditLogServiceFactoryDep): TAuditLogServiceFactory => {
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => { const listAuditLogs: TAuditLogServiceFactory["listAuditLogs"] = async ({
actorAuthMethod,
actorId,
actorOrgId,
actor,
filter
}) => {
// Filter logs for specific project // Filter logs for specific project
if (filter.projectId) { if (filter.projectId) {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@ -34,8 +37,7 @@ export const auditLogServiceFactory = ({
actorId, actorId,
projectId: filter.projectId, projectId: filter.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
} else { } else {
@ -65,7 +67,8 @@ export const auditLogServiceFactory = ({
secretPath: filter.secretPath, secretPath: filter.secretPath,
secretKey: filter.secretKey, secretKey: filter.secretKey,
environment: filter.environment, environment: filter.environment,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) orgId: actorOrgId,
...(filter.projectId ? { projectId: filter.projectId } : {})
}); });
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({ return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
@ -75,7 +78,7 @@ export const auditLogServiceFactory = ({
})); }));
}; };
const createAuditLog = async (data: TCreateAuditLogDTO) => { const createAuditLog: TAuditLogServiceFactory["createAuditLog"] = async (data) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (appCfg.DISABLE_AUDIT_LOG_GENERATION) { if (appCfg.DISABLE_AUDIT_LOG_GENERATION) {
return; return;

View File

@ -56,8 +56,8 @@ export type TListProjectAuditLogDTO = {
eventType?: EventType[]; eventType?: EventType[];
offset?: number; offset?: number;
limit: number; limit: number;
endDate?: string; endDate: string;
startDate?: string; startDate: string;
projectId?: string; projectId?: string;
environment?: string; environment?: string;
auditLogActorId?: string; auditLogActorId?: string;
@ -82,6 +82,32 @@ export type TCreateAuditLogDTO = {
projectId?: string; projectId?: string;
} & BaseAuthData; } & BaseAuthData;
export type TAuditLogServiceFactory = {
createAuditLog: (data: TCreateAuditLogDTO) => Promise<void>;
listAuditLogs: (arg: TListProjectAuditLogDTO) => Promise<
{
event: {
type: string;
metadata: unknown;
};
actor: {
type: string;
metadata: unknown;
};
id: string;
createdAt: Date;
updatedAt: Date;
orgId?: string | null | undefined;
userAgent?: string | null | undefined;
expiresAt?: Date | null | undefined;
ipAddress?: string | null | undefined;
userAgentType?: string | null | undefined;
projectId?: string | null | undefined;
projectName?: string | null | undefined;
}[]
>;
};
export type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">; export type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
interface BaseAuthData { interface BaseAuthData {
@ -176,6 +202,12 @@ export enum EventType {
REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth", REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth",
GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth", GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth",
LOGIN_IDENTITY_TLS_CERT_AUTH = "login-identity-tls-cert-auth",
ADD_IDENTITY_TLS_CERT_AUTH = "add-identity-tls-cert-auth",
UPDATE_IDENTITY_TLS_CERT_AUTH = "update-identity-tls-cert-auth",
REVOKE_IDENTITY_TLS_CERT_AUTH = "revoke-identity-tls-cert-auth",
GET_IDENTITY_TLS_CERT_AUTH = "get-identity-tls-cert-auth",
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth", LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth", ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth", UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
@ -754,6 +786,7 @@ interface CreateIdentityEvent {
metadata: { metadata: {
identityId: string; identityId: string;
name: string; name: string;
hasDeleteProtection: boolean;
}; };
} }
@ -762,6 +795,7 @@ interface UpdateIdentityEvent {
metadata: { metadata: {
identityId: string; identityId: string;
name?: string; name?: string;
hasDeleteProtection?: boolean;
}; };
} }
@ -1113,6 +1147,53 @@ interface GetIdentityAliCloudAuthEvent {
}; };
} }
interface LoginIdentityTlsCertAuthEvent {
type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH;
metadata: {
identityId: string;
identityTlsCertAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityTlsCertAuthEvent {
type: EventType.ADD_IDENTITY_TLS_CERT_AUTH;
metadata: {
identityId: string;
allowedCommonNames: string | null | undefined;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityTlsCertAuthEvent {
type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityTlsCertAuthEvent {
type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH;
metadata: {
identityId: string;
allowedCommonNames: string | null | undefined;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityTlsCertAuthEvent {
type: EventType.GET_IDENTITY_TLS_CERT_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityOciAuthEvent { interface LoginIdentityOciAuthEvent {
type: EventType.LOGIN_IDENTITY_OCI_AUTH; type: EventType.LOGIN_IDENTITY_OCI_AUTH;
metadata: { metadata: {
@ -3330,6 +3411,11 @@ export type Event =
| UpdateIdentityAliCloudAuthEvent | UpdateIdentityAliCloudAuthEvent
| GetIdentityAliCloudAuthEvent | GetIdentityAliCloudAuthEvent
| DeleteIdentityAliCloudAuthEvent | DeleteIdentityAliCloudAuthEvent
| LoginIdentityTlsCertAuthEvent
| AddIdentityTlsCertAuthEvent
| UpdateIdentityTlsCertAuthEvent
| GetIdentityTlsCertAuthEvent
| DeleteIdentityTlsCertAuthEvent
| LoginIdentityOciAuthEvent | LoginIdentityOciAuthEvent
| AddIdentityOciAuthEvent | AddIdentityOciAuthEvent
| UpdateIdentityOciAuthEvent | UpdateIdentityOciAuthEvent

View File

@ -1,10 +1,10 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { ormify, TOrmify } from "@app/lib/knex";
export type TCertificateAuthorityCrlDALFactory = ReturnType<typeof certificateAuthorityCrlDALFactory>; export type TCertificateAuthorityCrlDALFactory = TOrmify<TableName.CertificateAuthorityCrl>;
export const certificateAuthorityCrlDALFactory = (db: TDbClient) => { export const certificateAuthorityCrlDALFactory = (db: TDbClient): TCertificateAuthorityCrlDALFactory => {
const caCrlOrm = ormify(db, TableName.CertificateAuthorityCrl); const caCrlOrm = ormify(db, TableName.CertificateAuthorityCrl);
return caCrlOrm; return caCrlOrm;
}; };

View File

@ -1,9 +1,8 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
@ -12,7 +11,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types"; import { TCertificateAuthorityCrlServiceFactory } from "./certificate-authority-crl-types";
type TCertificateAuthorityCrlServiceFactoryDep = { type TCertificateAuthorityCrlServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
@ -22,19 +21,17 @@ type TCertificateAuthorityCrlServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
}; };
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
export const certificateAuthorityCrlServiceFactory = ({ export const certificateAuthorityCrlServiceFactory = ({
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService // licenseService permissionService // licenseService
}: TCertificateAuthorityCrlServiceFactoryDep) => { }: TCertificateAuthorityCrlServiceFactoryDep): TCertificateAuthorityCrlServiceFactory => {
/** /**
* Return CRL with id [crlId] * Return CRL with id [crlId]
*/ */
const getCrlById = async (crlId: TGetCrlById) => { const getCrlById: TCertificateAuthorityCrlServiceFactory["getCrlById"] = async (crlId) => {
const caCrl = await certificateAuthorityCrlDAL.findById(crlId); const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` }); if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` });
@ -65,7 +62,13 @@ export const certificateAuthorityCrlServiceFactory = ({
/** /**
* Returns a list of CRL ids for CA with id [caId] * Returns a list of CRL ids for CA with id [caId]
*/ */
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => { const getCaCrls: TCertificateAuthorityCrlServiceFactory["getCaCrls"] = async ({
caId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
@ -74,8 +77,7 @@ export const certificateAuthorityCrlServiceFactory = ({
actorId, actorId,
projectId: ca.projectId, projectId: ca.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(

View File

@ -5,3 +5,137 @@ export type TGetCrlById = string;
export type TGetCaCrlsDTO = { export type TGetCaCrlsDTO = {
caId: string; caId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCertificateAuthorityCrlServiceFactory = {
getCrlById: (crlId: TGetCrlById) => Promise<{
ca: {
readonly requireTemplateForIssuance: boolean;
readonly internalCa:
| {
id: string;
parentCaId: string | null | undefined;
type: string;
friendlyName: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
commonName: string;
dn: string;
serialNumber: string | null | undefined;
maxPathLength: number | null | undefined;
keyAlgorithm: string;
notBefore: string | undefined;
notAfter: string | undefined;
activeCaCertId: string | null | undefined;
}
| undefined;
readonly externalCa:
| {
id: string;
type: string;
configuration: unknown;
dnsAppConnectionId: string | null | undefined;
appConnectionId: string | null | undefined;
credentials: Buffer | null | undefined;
}
| undefined;
readonly name: string;
readonly status: string;
readonly id: string;
readonly createdAt: Date;
readonly updatedAt: Date;
readonly projectId: string;
readonly enableDirectIssuance: boolean;
readonly parentCaId: string | null | undefined;
readonly type: string;
readonly friendlyName: string;
readonly organization: string;
readonly ou: string;
readonly country: string;
readonly province: string;
readonly locality: string;
readonly commonName: string;
readonly dn: string;
readonly serialNumber: string | null | undefined;
readonly maxPathLength: number | null | undefined;
readonly keyAlgorithm: string;
readonly notBefore: string | undefined;
readonly notAfter: string | undefined;
readonly activeCaCertId: string | null | undefined;
};
caCrl: {
id: string;
createdAt: Date;
updatedAt: Date;
caId: string;
caSecretId: string;
encryptedCrl: Buffer;
};
crl: ArrayBuffer;
}>;
getCaCrls: ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => Promise<{
ca: {
readonly requireTemplateForIssuance: boolean;
readonly internalCa:
| {
id: string;
parentCaId: string | null | undefined;
type: string;
friendlyName: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
commonName: string;
dn: string;
serialNumber: string | null | undefined;
maxPathLength: number | null | undefined;
keyAlgorithm: string;
notBefore: string | undefined;
notAfter: string | undefined;
activeCaCertId: string | null | undefined;
}
| undefined;
readonly externalCa:
| {
id: string;
type: string;
configuration: unknown;
dnsAppConnectionId: string | null | undefined;
appConnectionId: string | null | undefined;
credentials: Buffer | null | undefined;
}
| undefined;
readonly name: string;
readonly status: string;
readonly id: string;
readonly createdAt: Date;
readonly updatedAt: Date;
readonly projectId: string;
readonly enableDirectIssuance: boolean;
readonly parentCaId: string | null | undefined;
readonly type: string;
readonly friendlyName: string;
readonly organization: string;
readonly ou: string;
readonly country: string;
readonly province: string;
readonly locality: string;
readonly commonName: string;
readonly dn: string;
readonly serialNumber: string | null | undefined;
readonly maxPathLength: number | null | undefined;
readonly keyAlgorithm: string;
readonly notBefore: string | undefined;
readonly notAfter: string | undefined;
readonly activeCaCertId: string | null | undefined;
};
crls: {
id: string;
crl: string;
}[];
}>;
};

View File

@ -3,9 +3,43 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas"; import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
export type TDynamicSecretLeaseDALFactory = ReturnType<typeof dynamicSecretLeaseDALFactory>; export interface TDynamicSecretLeaseDALFactory extends Omit<TOrmify<TableName.DynamicSecretLease>, "findById"> {
countLeasesForDynamicSecret: (dynamicSecretId: string, tx?: Knex) => Promise<number>;
findById: (
id: string,
tx?: Knex
) => Promise<
| {
dynamicSecret: {
id: string;
name: string;
version: number;
type: string;
defaultTTL: string;
maxTTL: string | null | undefined;
encryptedInput: Buffer;
folderId: string;
status: string | null | undefined;
statusDetails: string | null | undefined;
createdAt: Date;
updatedAt: Date;
};
version: number;
id: string;
createdAt: Date;
updatedAt: Date;
externalEntityId: string;
expireAt: Date;
dynamicSecretId: string;
status?: string | null | undefined;
config?: unknown;
statusDetails?: string | null | undefined;
}
| undefined
>;
}
export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecretLease); const orm = ormify(db, TableName.DynamicSecretLease);

View File

@ -21,7 +21,12 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findById">; folderDAL: Pick<TSecretFolderDALFactory, "findById">;
}; };
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>; export type TDynamicSecretLeaseQueueServiceFactory = {
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise<void>;
unsetLeaseRevocation: (leaseId: string) => Promise<void>;
init: () => Promise<void>;
};
export const dynamicSecretLeaseQueueServiceFactory = ({ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService, queueService,
@ -30,55 +35,48 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
kmsService, kmsService,
folderDAL folderDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => { }: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue( await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueName.DynamicSecretRevocation,
QueueJobs.DynamicSecretPruning, QueueJobs.DynamicSecretPruning,
{ dynamicSecretCfgId }, { dynamicSecretCfgId },
{ {
jobId: dynamicSecretCfgId, singletonKey: dynamicSecretCfgId,
backoff: { retryLimit: 3,
type: "exponential", retryBackoff: true
delay: 3000
},
removeOnFail: {
count: 3
},
removeOnComplete: true
} }
); );
}; };
const setLeaseRevocation = async (leaseId: string, expiry: number) => { const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => {
await queueService.queue( await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueName.DynamicSecretRevocation,
QueueJobs.DynamicSecretRevocation, QueueJobs.DynamicSecretRevocation,
{ leaseId }, { leaseId },
{ {
jobId: leaseId, id: leaseId,
backoff: { singletonKey: leaseId,
type: "exponential", startAfter: expiryAt,
delay: 3000 retryLimit: 3,
}, retryBackoff: true,
delay: expiry, retentionDays: 2
removeOnFail: {
count: 3
},
removeOnComplete: true
} }
); );
}; };
const unsetLeaseRevocation = async (leaseId: string) => { const unsetLeaseRevocation = async (leaseId: string) => {
await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId); await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId);
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
}; };
queueService.start(QueueName.DynamicSecretRevocation, async (job) => { const $dynamicSecretQueueJob = async (
jobName: string,
jobId: string,
data: { leaseId: string } | { dynamicSecretCfgId: string }
): Promise<void> => {
try { try {
if (job.name === QueueJobs.DynamicSecretRevocation) { if (jobName === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string }; const { leaseId } = data as { leaseId: string };
logger.info("Dynamic secret lease revocation started: ", leaseId, job.id); logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
@ -107,9 +105,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
return; return;
} }
if (job.name === QueueJobs.DynamicSecretPruning) { if (jobName === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string }; const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id); logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, jobId);
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId); const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
@ -150,38 +148,68 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
await dynamicSecretDAL.deleteById(dynamicSecretCfgId); await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
} }
logger.info("Finished dynamic secret job", job.id); logger.info("Finished dynamic secret job", jobId);
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
if (job?.name === QueueJobs.DynamicSecretPruning) { if (jobName === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string }; const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
await dynamicSecretDAL.updateById(dynamicSecretCfgId, { await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
status: DynamicSecretStatus.FailedDeletion, status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255) statusDetails: (error as Error)?.message?.slice(0, 255)
}); });
} }
if (job?.name === QueueJobs.DynamicSecretRevocation) { if (jobName === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string }; const { leaseId } = data as { leaseId: string };
await dynamicSecretLeaseDAL.updateById(leaseId, { await dynamicSecretLeaseDAL.updateById(leaseId, {
status: DynamicSecretStatus.FailedDeletion, status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255) statusDetails: (error as Error)?.message?.slice(0, 255)
}); });
} }
if (error instanceof DisableRotationErrors) { if (error instanceof DisableRotationErrors) {
if (job.id) { if (jobId) {
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id); await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
} }
} }
// propogate to next part // propogate to next part
throw error; throw error;
} }
};
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
}); });
const init = async () => {
await queueService.startPg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretRevocation,
async ([job]) => {
await $dynamicSecretQueueJob(job.name, job.id, job.data);
},
{
workerCount: 5,
pollingIntervalSeconds: 1
}
);
await queueService.startPg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretPruning,
async ([job]) => {
await $dynamicSecretQueueJob(job.name, job.id, job.data);
},
{
workerCount: 1,
pollingIntervalSeconds: 1
}
);
};
return { return {
pruneDynamicSecret, pruneDynamicSecret,
setLeaseRevocation, setLeaseRevocation,
unsetLeaseRevocation unsetLeaseRevocation,
init
}; };
}; };

View File

@ -1,9 +1,8 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import RE2 from "re2"; import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { import {
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionSub ProjectPermissionSub
@ -26,12 +25,8 @@ import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue"; import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
import { import {
DynamicSecretLeaseStatus, DynamicSecretLeaseStatus,
TCreateDynamicSecretLeaseDTO,
TDeleteDynamicSecretLeaseDTO,
TDetailsDynamicSecretLeaseDTO,
TDynamicSecretLeaseConfig, TDynamicSecretLeaseConfig,
TListDynamicSecretLeasesDTO, TDynamicSecretLeaseServiceFactory
TRenewDynamicSecretLeaseDTO
} from "./dynamic-secret-lease-types"; } from "./dynamic-secret-lease-types";
type TDynamicSecretLeaseServiceFactoryDep = { type TDynamicSecretLeaseServiceFactoryDep = {
@ -48,8 +43,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
identityDAL: TIdentityDALFactory; identityDAL: TIdentityDALFactory;
}; };
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
export const dynamicSecretLeaseServiceFactory = ({ export const dynamicSecretLeaseServiceFactory = ({
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
@ -62,14 +55,14 @@ export const dynamicSecretLeaseServiceFactory = ({
kmsService, kmsService,
userDAL, userDAL,
identityDAL identityDAL
}: TDynamicSecretLeaseServiceFactoryDep) => { }: TDynamicSecretLeaseServiceFactoryDep): TDynamicSecretLeaseServiceFactory => {
const extractEmailUsername = (email: string) => { const extractEmailUsername = (email: string) => {
const regex = new RE2(/^([^@]+)/); const regex = new RE2(/^([^@]+)/);
const match = email.match(regex); const match = email.match(regex);
return match ? match[1] : email; return match ? match[1] : email;
}; };
const create = async ({ const create: TDynamicSecretLeaseServiceFactory["create"] = async ({
environmentSlug, environmentSlug,
path, path,
name, name,
@ -80,7 +73,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorAuthMethod, actorAuthMethod,
ttl, ttl,
config config
}: TCreateDynamicSecretLeaseDTO) => { }) => {
const appCfg = getConfig(); const appCfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -91,8 +84,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@ -184,11 +176,11 @@ export const dynamicSecretLeaseServiceFactory = ({
config config
}); });
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date())); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data }; return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
}; };
const renewLease = async ({ const renewLease: TDynamicSecretLeaseServiceFactory["renewLease"] = async ({
ttl, ttl,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
@ -198,7 +190,7 @@ export const dynamicSecretLeaseServiceFactory = ({
path, path,
environmentSlug, environmentSlug,
leaseId leaseId
}: TRenewDynamicSecretLeaseDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -208,8 +200,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
@ -278,7 +269,7 @@ export const dynamicSecretLeaseServiceFactory = ({
); );
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date())); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
expireAt, expireAt,
externalEntityId: entityId externalEntityId: entityId
@ -286,7 +277,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return updatedDynamicSecretLease; return updatedDynamicSecretLease;
}; };
const revokeLease = async ({ const revokeLease: TDynamicSecretLeaseServiceFactory["revokeLease"] = async ({
leaseId, leaseId,
environmentSlug, environmentSlug,
path, path,
@ -296,7 +287,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
isForced isForced
}: TDeleteDynamicSecretLeaseDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -306,8 +297,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
@ -376,7 +366,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return deletedDynamicSecretLease; return deletedDynamicSecretLease;
}; };
const listLeases = async ({ const listLeases: TDynamicSecretLeaseServiceFactory["listLeases"] = async ({
path, path,
name, name,
actor, actor,
@ -385,7 +375,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
environmentSlug, environmentSlug,
actorAuthMethod actorAuthMethod
}: TListDynamicSecretLeasesDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -395,8 +385,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -424,7 +413,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return dynamicSecretLeases; return dynamicSecretLeases;
}; };
const getLeaseDetails = async ({ const getLeaseDetails: TDynamicSecretLeaseServiceFactory["getLeaseDetails"] = async ({
projectSlug, projectSlug,
actorOrgId, actorOrgId,
path, path,
@ -433,7 +422,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
leaseId, leaseId,
actorAuthMethod actorAuthMethod
}: TDetailsDynamicSecretLeaseDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -443,8 +432,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);

View File

@ -1,4 +1,5 @@
import { TProjectPermission } from "@app/lib/types"; import { TDynamicSecretLeases } from "@app/db/schemas";
import { TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
export enum DynamicSecretLeaseStatus { export enum DynamicSecretLeaseStatus {
FailedDeletion = "Failed to delete" FailedDeletion = "Failed to delete"
@ -48,3 +49,40 @@ export type TDynamicSecretKubernetesLeaseConfig = {
}; };
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig; export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
export type TDynamicSecretLeaseServiceFactory = {
create: (arg: TCreateDynamicSecretLeaseDTO) => Promise<{
lease: TDynamicSecretLeases;
dynamicSecret: TDynamicSecretWithMetadata;
data: unknown;
}>;
listLeases: (arg: TListDynamicSecretLeasesDTO) => Promise<TDynamicSecretLeases[]>;
revokeLease: (arg: TDeleteDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
renewLease: (arg: TRenewDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
getLeaseDetails: (arg: TDetailsDynamicSecretLeaseDTO) => Promise<{
dynamicSecret: {
id: string;
name: string;
version: number;
type: string;
defaultTTL: string;
maxTTL: string | null | undefined;
encryptedInput: Buffer;
folderId: string;
status: string | null | undefined;
statusDetails: string | null | undefined;
createdAt: Date;
updatedAt: Date;
};
version: number;
id: string;
createdAt: Date;
updatedAt: Date;
externalEntityId: string;
expireAt: Date;
dynamicSecretId: string;
status?: string | null | undefined;
config?: unknown;
statusDetails?: string | null | undefined;
}>;
};

View File

@ -10,17 +10,35 @@ import {
selectAllTableCols, selectAllTableCols,
sqlNestRelationships, sqlNestRelationships,
TFindFilter, TFindFilter,
TFindOpt TFindOpt,
TOrmify
} from "@app/lib/knex"; } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection, TDynamicSecretWithMetadata } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>; export interface TDynamicSecretDALFactory extends Omit<TOrmify<TableName.DynamicSecret>, "findOne"> {
findOne: (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => Promise<TDynamicSecretWithMetadata>;
listDynamicSecretsByFolderIds: (
arg: {
folderIds: string[];
search?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
orderBy?: SecretsOrderBy | undefined;
orderDirection?: OrderByDirection | undefined;
},
tx?: Knex
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
findWithMetadata: (
filter: TFindFilter<TDynamicSecrets>,
arg?: TFindOpt<TDynamicSecrets>
) => Promise<TDynamicSecretWithMetadata[]>;
}
export const dynamicSecretDALFactory = (db: TDbClient) => { export const dynamicSecretDALFactory = (db: TDbClient): TDynamicSecretDALFactory => {
const orm = ormify(db, TableName.DynamicSecret); const orm = ormify(db, TableName.DynamicSecret);
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => { const findOne: TDynamicSecretDALFactory["findOne"] = async (filter, tx) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret) const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin( .leftJoin(
TableName.ResourceMetadata, TableName.ResourceMetadata,
@ -55,9 +73,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
return docs[0]; return docs[0];
}; };
const findWithMetadata = async ( const findWithMetadata: TDynamicSecretDALFactory["findWithMetadata"] = async (
filter: TFindFilter<TDynamicSecrets>, filter,
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {} { offset, limit, sort, tx } = {}
) => { ) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret) const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin( .leftJoin(
@ -101,23 +119,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
}; };
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination) // find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
const listDynamicSecretsByFolderIds = async ( const listDynamicSecretsByFolderIds: TDynamicSecretDALFactory["listDynamicSecretsByFolderIds"] = async (
{ { folderIds, search, limit, offset = 0, orderBy = SecretsOrderBy.Name, orderDirection = OrderByDirection.ASC },
folderIds, tx
search,
limit,
offset = 0,
orderBy = SecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC
}: {
folderIds: string[];
search?: string;
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
},
tx?: Knex
) => { ) => {
try { try {
const query = (tx || db.replicaNode())(TableName.DynamicSecret) const query = (tx || db.replicaNode())(TableName.DynamicSecret)

View File

@ -1,14 +1,13 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { import {
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@ -20,17 +19,7 @@ import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/
import { TGatewayDALFactory } from "../gateway/gateway-dal"; import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
DynamicSecretStatus,
TCreateDynamicSecretDTO,
TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO,
TListDynamicSecretsByFolderMappingsDTO,
TListDynamicSecretsDTO,
TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO
} from "./dynamic-secret-types";
import { AzureEntraIDProvider } from "./providers/azure-entra-id"; import { AzureEntraIDProvider } from "./providers/azure-entra-id";
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
@ -51,8 +40,6 @@ type TDynamicSecretServiceFactoryDep = {
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
export const dynamicSecretServiceFactory = ({ export const dynamicSecretServiceFactory = ({
dynamicSecretDAL, dynamicSecretDAL,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
@ -65,8 +52,8 @@ export const dynamicSecretServiceFactory = ({
kmsService, kmsService,
gatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
const create = async ({ const create: TDynamicSecretServiceFactory["create"] = async ({
path, path,
actor, actor,
name, name,
@ -80,7 +67,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod, actorAuthMethod,
metadata, metadata,
usernameTemplate usernameTemplate
}: TCreateDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -90,8 +77,7 @@ export const dynamicSecretServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -188,7 +174,7 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
const updateByName = async ({ const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
name, name,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
@ -203,7 +189,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod, actorAuthMethod,
metadata, metadata,
usernameTemplate usernameTemplate
}: TUpdateDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -214,8 +200,7 @@ export const dynamicSecretServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@ -345,7 +330,7 @@ export const dynamicSecretServiceFactory = ({
return updatedDynamicCfg; return updatedDynamicCfg;
}; };
const deleteByName = async ({ const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -355,7 +340,7 @@ export const dynamicSecretServiceFactory = ({
path, path,
environmentSlug, environmentSlug,
isForced isForced
}: TDeleteDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -366,8 +351,7 @@ export const dynamicSecretServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -413,7 +397,7 @@ export const dynamicSecretServiceFactory = ({
return deletedDynamicSecretCfg; return deletedDynamicSecretCfg;
}; };
const getDetails = async ({ const getDetails: TDynamicSecretServiceFactory["getDetails"] = async ({
name, name,
projectSlug, projectSlug,
path, path,
@ -422,7 +406,7 @@ export const dynamicSecretServiceFactory = ({
actorOrgId, actorOrgId,
actorId, actorId,
actor actor
}: TDetailsDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -432,8 +416,7 @@ export const dynamicSecretServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -480,7 +463,7 @@ export const dynamicSecretServiceFactory = ({
}; };
// get unique dynamic secret count across multiple envs // get unique dynamic secret count across multiple envs
const getCountMultiEnv = async ({ const getCountMultiEnv: TDynamicSecretServiceFactory["getCountMultiEnv"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -490,15 +473,14 @@ export const dynamicSecretServiceFactory = ({
environmentSlugs, environmentSlugs,
search, search,
isInternal isInternal
}: TListDynamicSecretsMultiEnvDTO) => { }) => {
if (!isInternal) { if (!isInternal) {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
// verify user has access to each env in request // verify user has access to each env in request
@ -526,7 +508,7 @@ export const dynamicSecretServiceFactory = ({
}; };
// get dynamic secret count for a single env // get dynamic secret count for a single env
const getDynamicSecretCount = async ({ const getDynamicSecretCount: TDynamicSecretServiceFactory["getDynamicSecretCount"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -535,14 +517,13 @@ export const dynamicSecretServiceFactory = ({
environmentSlug, environmentSlug,
search, search,
projectId projectId
}: TGetDynamicSecretsCountDTO) => { }) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionDynamicSecretActions.ReadRootCredential,
@ -561,7 +542,7 @@ export const dynamicSecretServiceFactory = ({
return Number(dynamicSecretCfg[0]?.count ?? 0); return Number(dynamicSecretCfg[0]?.count ?? 0);
}; };
const listDynamicSecretsByEnv = async ({ const listDynamicSecretsByEnv: TDynamicSecretServiceFactory["listDynamicSecretsByEnv"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -575,7 +556,7 @@ export const dynamicSecretServiceFactory = ({
orderDirection = OrderByDirection.ASC, orderDirection = OrderByDirection.ASC,
search, search,
...params ...params
}: TListDynamicSecretsDTO) => { }) => {
let { projectId } = params; let { projectId } = params;
if (!projectId) { if (!projectId) {
@ -590,8 +571,7 @@ export const dynamicSecretServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@ -619,17 +599,16 @@ export const dynamicSecretServiceFactory = ({
}); });
}; };
const listDynamicSecretsByFolderIds = async ( const listDynamicSecretsByFolderIds: TDynamicSecretServiceFactory["listDynamicSecretsByFolderIds"] = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO, { folderMappings, filters, projectId },
actor: OrgServiceActor actor
) => { ) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
actorId: actor.id, actorId: actor.id,
projectId, projectId,
actorAuthMethod: actor.authMethod, actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId, actorOrgId: actor.orgId
actionProjectType: ActionProjectType.SecretManager
}); });
const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) => const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) =>
@ -657,7 +636,7 @@ export const dynamicSecretServiceFactory = ({
}; };
// get dynamic secrets for multiple envs // get dynamic secrets for multiple envs
const listDynamicSecretsByEnvs = async ({ const listDynamicSecretsByEnvs: TDynamicSecretServiceFactory["listDynamicSecretsByEnvs"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -667,14 +646,13 @@ export const dynamicSecretServiceFactory = ({
projectId, projectId,
isInternal, isInternal,
...params ...params
}: TListDynamicSecretsMultiEnvDTO) => { }) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
@ -700,14 +678,10 @@ export const dynamicSecretServiceFactory = ({
}); });
}; };
const fetchAzureEntraIdUsers = async ({ const fetchAzureEntraIdUsers: TDynamicSecretServiceFactory["fetchAzureEntraIdUsers"] = async ({
tenantId, tenantId,
applicationId, applicationId,
clientSecret clientSecret
}: {
tenantId: string;
applicationId: string;
clientSecret: string;
}) => { }) => {
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers( const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
tenantId, tenantId,

View File

@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TDynamicSecrets } from "@app/db/schemas";
import { OrderByDirection, OrgServiceActor, TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@ -83,3 +84,27 @@ export type TListDynamicSecretsMultiEnvDTO = Omit<
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & { export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
projectId: string; projectId: string;
}; };
export type TDynamicSecretServiceFactory = {
create: (arg: TCreateDynamicSecretDTO) => Promise<TDynamicSecrets>;
updateByName: (arg: TUpdateDynamicSecretDTO) => Promise<TDynamicSecrets>;
deleteByName: (arg: TDeleteDynamicSecretDTO) => Promise<TDynamicSecrets>;
getDetails: (arg: TDetailsDynamicSecretDTO) => Promise<TDynamicSecretWithMetadata>;
listDynamicSecretsByEnv: (arg: TListDynamicSecretsDTO) => Promise<TDynamicSecretWithMetadata[]>;
listDynamicSecretsByEnvs: (
arg: TListDynamicSecretsMultiEnvDTO
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
getDynamicSecretCount: (arg: TGetDynamicSecretsCountDTO) => Promise<number>;
getCountMultiEnv: (arg: TListDynamicSecretsMultiEnvDTO) => Promise<number>;
fetchAzureEntraIdUsers: (arg: { tenantId: string; applicationId: string; clientSecret: string }) => Promise<
{
name: string;
id: string;
email: string;
}[]
>;
listDynamicSecretsByFolderIds: (
arg: TListDynamicSecretsByFolderMappingsDTO,
actor: OrgServiceActor
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string; path: string }>>;
};

View File

@ -128,11 +128,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity); const username = generateUsername(usernameTemplate, identity);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
const additionalTags = providerInputs.tags.map((tag) => ({
Key: tag.key,
Value: tag.value
}));
awsTags.push(...additionalTags);
}
const createUserRes = await client.send( const createUserRes = await client.send(
new CreateUserCommand({ new CreateUserCommand({
Path: awsPath, Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined, PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: [{ Key: "createdBy", Value: "infisical-dynamic-secret" }], Tags: awsTags,
UserName: username UserName: username
}) })
); );

View File

@ -0,0 +1,133 @@
import axios from "axios";
import jwt from "jsonwebtoken";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { DynamicSecretGithubSchema, TDynamicProviderFns } from "./models";
interface GitHubInstallationTokenResponse {
token: string;
expires_at: string; // ISO 8601 timestamp e.g., "2024-01-15T12:00:00Z"
permissions?: Record<string, string>;
repository_selection?: string;
}
interface TGithubProviderInputs {
appId: number;
installationId: number;
privateKey: string;
}
export const GithubProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretGithubSchema.parseAsync(inputs);
return providerInputs;
};
const $generateGitHubInstallationAccessToken = async (
credentials: TGithubProviderInputs
): Promise<GitHubInstallationTokenResponse> => {
const { appId, installationId, privateKey } = credentials;
const nowInSeconds = Math.floor(Date.now() / 1000);
const jwtPayload = {
iat: nowInSeconds - 5,
exp: nowInSeconds + 60,
iss: String(appId)
};
let appJwt: string;
try {
appJwt = jwt.sign(jwtPayload, privateKey, { algorithm: "RS256" });
} catch (error) {
let message = "Failed to sign JWT.";
if (error instanceof jwt.JsonWebTokenError) {
message += ` JsonWebTokenError: ${error.message}`;
}
throw new InternalServerError({
message
});
}
const tokenUrl = `${IntegrationUrls.GITHUB_API_URL}/app/installations/${String(installationId)}/access_tokens`;
try {
const response = await axios.post<GitHubInstallationTokenResponse>(tokenUrl, undefined, {
headers: {
Authorization: `Bearer ${appJwt}`,
Accept: "application/vnd.github.v3+json",
"X-GitHub-Api-Version": "2022-11-28"
}
});
if (response.status === 201 && response.data.token) {
return response.data; // Includes token, expires_at, permissions, repository_selection
}
throw new InternalServerError({
message: `GitHub API responded with unexpected status ${response.status}: ${JSON.stringify(response.data)}`
});
} catch (error) {
let message = "Failed to fetch GitHub installation access token.";
if (axios.isAxiosError(error) && error.response) {
const githubErrorMsg =
(error.response.data as { message?: string })?.message || JSON.stringify(error.response.data);
message += ` GitHub API Error: ${error.response.status} - ${githubErrorMsg}`;
// Classify as BadRequestError for auth-related issues (401, 403, 404) which might be due to user input
if ([401, 403, 404].includes(error.response.status)) {
throw new BadRequestError({ message });
}
}
throw new InternalServerError({ message });
}
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await $generateGitHubInstallationAccessToken(providerInputs);
return true;
};
const create = async (data: { inputs: unknown }) => {
const { inputs } = data;
const providerInputs = await validateProviderInputs(inputs);
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
const entityId = alphaNumericNanoId(32);
return {
entityId,
data: {
TOKEN: ghTokenData.token,
EXPIRES_AT: ghTokenData.expires_at,
PERMISSIONS: ghTokenData.permissions,
REPOSITORY_SELECTION: ghTokenData.repository_selection
}
};
};
const revoke = async () => {
// GitHub installation tokens cannot be revoked.
throw new BadRequestError({
message:
"Github dynamic secret does not support revocation because GitHub itself cannot revoke installation tokens"
});
};
const renew = async () => {
// No renewal
throw new BadRequestError({ message: "Github dynamic secret does not support renewal" });
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -7,6 +7,7 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra"; import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search"; import { ElasticSearchProvider } from "./elastic-search";
import { GcpIamProvider } from "./gcp-iam"; import { GcpIamProvider } from "./gcp-iam";
import { GithubProvider } from "./github";
import { KubernetesProvider } from "./kubernetes"; import { KubernetesProvider } from "./kubernetes";
import { LdapProvider } from "./ldap"; import { LdapProvider } from "./ldap";
import { DynamicSecretProviders, TDynamicProviderFns } from "./models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
@ -44,5 +45,6 @@ export const buildDynamicSecretProviders = ({
[DynamicSecretProviders.SapAse]: SapAseProvider(), [DynamicSecretProviders.SapAse]: SapAseProvider(),
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }), [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }), [DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
[DynamicSecretProviders.GcpIam]: GcpIamProvider() [DynamicSecretProviders.GcpIam]: GcpIamProvider(),
[DynamicSecretProviders.Github]: GithubProvider()
}); });

View File

@ -52,9 +52,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: string; gatewayId: string;
targetHost: string; targetHost: string;
targetPort: number; targetPort: number;
caCert?: string; httpsAgent?: https.Agent;
reviewTokenThroughGateway: boolean; reviewTokenThroughGateway: boolean;
enableSsl: boolean;
}, },
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T> gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
): Promise<T> => { ): Promise<T> => {
@ -85,10 +84,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
key: relayDetails.privateKey.toString() key: relayDetails.privateKey.toString()
}, },
// we always pass this, because its needed for both tcp and http protocol // we always pass this, because its needed for both tcp and http protocol
httpsAgent: new https.Agent({ httpsAgent: inputs.httpsAgent
ca: inputs.caCert,
rejectUnauthorized: inputs.enableSsl
})
} }
); );
@ -311,6 +307,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const k8sHost = `${url.protocol}//${url.hostname}`; const k8sHost = `${url.protocol}//${url.hostname}`;
try { try {
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) { if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper( await $gatewayProxyWrapper(
@ -318,8 +322,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sHost, targetHost: k8sHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: true reviewTokenThroughGateway: true
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -332,8 +335,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost, targetHost: k8sGatewayHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: false reviewTokenThroughGateway: false
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -342,9 +344,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
); );
} }
} else if (providerInputs.credentialType === KubernetesCredentialType.Static) { } else if (providerInputs.credentialType === KubernetesCredentialType.Static) {
await serviceAccountStaticCallback(k8sHost, k8sPort); await serviceAccountStaticCallback(k8sHost, k8sPort, httpsAgent);
} else { } else {
await serviceAccountDynamicCallback(k8sHost, k8sPort); await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
} }
return true; return true;
@ -546,6 +548,15 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
try { try {
let tokenData; let tokenData;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) { if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
tokenData = await $gatewayProxyWrapper( tokenData = await $gatewayProxyWrapper(
@ -553,8 +564,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sHost, targetHost: k8sHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: true reviewTokenThroughGateway: true
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -567,8 +577,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost, targetHost: k8sGatewayHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: false reviewTokenThroughGateway: false
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -579,8 +588,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
} else { } else {
tokenData = tokenData =
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
? await tokenRequestStaticCallback(k8sHost, k8sPort) ? await tokenRequestStaticCallback(k8sHost, k8sPort, httpsAgent)
: await serviceAccountDynamicCallback(k8sHost, k8sPort); : await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
} }
return { return {
@ -684,6 +693,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const k8sPort = url.port ? Number(url.port) : 443; const k8sPort = url.port ? Number(url.port) : 443;
const k8sHost = `${url.protocol}//${url.hostname}`; const k8sHost = `${url.protocol}//${url.hostname}`;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) { if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper( await $gatewayProxyWrapper(
@ -691,8 +708,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sHost, targetHost: k8sHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: true reviewTokenThroughGateway: true
}, },
serviceAccountDynamicCallback serviceAccountDynamicCallback
@ -703,15 +719,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost, targetHost: k8sGatewayHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: false reviewTokenThroughGateway: false
}, },
serviceAccountDynamicCallback serviceAccountDynamicCallback
); );
} }
} else { } else {
await serviceAccountDynamicCallback(k8sHost, k8sPort); await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
} }
} }

View File

@ -2,6 +2,7 @@ import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types"; import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
@ -207,7 +208,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional() policyArns: z.string().trim().optional(),
tags: ResourceMetadataSchema.optional()
}), }),
z.object({ z.object({
method: z.literal(AwsIamAuthType.AssumeRole), method: z.literal(AwsIamAuthType.AssumeRole),
@ -217,7 +219,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional() policyArns: z.string().trim().optional(),
tags: ResourceMetadataSchema.optional()
}) })
]) ])
); );
@ -474,6 +477,23 @@ export const DynamicSecretGcpIamSchema = z.object({
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128) serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128)
}); });
export const DynamicSecretGithubSchema = z.object({
appId: z.number().min(1).describe("The ID of your GitHub App."),
installationId: z.number().min(1).describe("The ID of the GitHub App installation."),
privateKey: z
.string()
.trim()
.min(1)
.refine(
(val) =>
new RE2(
/^-----BEGIN(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----\s*[\s\S]*?-----END(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----$/
).test(val),
"Invalid PEM format for private key"
)
.describe("The private key generated for your GitHub App.")
});
export enum DynamicSecretProviders { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
Cassandra = "cassandra", Cassandra = "cassandra",
@ -492,7 +512,8 @@ export enum DynamicSecretProviders {
SapAse = "sap-ase", SapAse = "sap-ase",
Kubernetes = "kubernetes", Kubernetes = "kubernetes",
Vertica = "vertica", Vertica = "vertica",
GcpIam = "gcp-iam" GcpIam = "gcp-iam",
Github = "github"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@ -513,7 +534,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }), z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }), z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }), z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }) z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema })
]); ]);
export type TDynamicProviderFns = { export type TDynamicProviderFns = {

View File

@ -11,7 +11,7 @@ import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TExternalKmsDALFactory } from "./external-kms-dal"; import { TExternalKmsDALFactory } from "./external-kms-dal";
import { import {
TCreateExternalKmsDTO, TCreateExternalKmsDTO,

View File

@ -21,7 +21,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TGatewayDALFactory } from "./gateway-dal"; import { TGatewayDALFactory } from "./gateway-dal";
import { import {
TExchangeAllocatedRelayAddressDTO, TExchangeAllocatedRelayAddressDTO,

View File

@ -14,7 +14,7 @@ import { TGroupDALFactory } from "../group/group-dal";
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal"; import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal";
import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types"; import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types";

View File

@ -169,11 +169,29 @@ export const groupDALFactory = (db: TDbClient) => {
} }
}; };
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.Groups)
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.where(`${TableName.Groups}.id`, id)
.select(
selectAllTableCols(TableName.Groups),
db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles)
)
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
};
return { return {
...groupOrm,
findGroups, findGroups,
findByOrgId, findByOrgId,
findAllGroupPossibleMembers, findAllGroupPossibleMembers,
findGroupsByProjectId, findGroupsByProjectId,
...groupOrm findById
}; };
}; };

View File

@ -15,7 +15,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TGroupDALFactory } from "./group-dal"; import { TGroupDALFactory } from "./group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
import { import {

View File

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@ -11,7 +11,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal";
import { import {
@ -61,8 +61,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit, ProjectPermissionIdentityActions.Edit,
@ -73,8 +72,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId: identityId, actorId: identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
@ -160,8 +158,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit, ProjectPermissionIdentityActions.Edit,
@ -172,8 +169,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId: identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
@ -260,8 +256,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit, ProjectPermissionIdentityActions.Edit,
@ -272,8 +267,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId: identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
const permissionBoundary = validatePrivilegeChangeOperation( const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem, membership.shouldUseNewPrivilegeSystem,
@ -321,8 +315,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read, ProjectPermissionIdentityActions.Read,
@ -356,8 +349,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read, ProjectPermissionIdentityActions.Read,
@ -392,8 +384,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read, ProjectPermissionIdentityActions.Read,

View File

@ -1,7 +1,6 @@
import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@ -11,7 +10,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { import {
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
ProjectPermissionSet, ProjectPermissionSet,
@ -73,8 +72,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -87,8 +85,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId: identityId, actorId: identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
@ -175,8 +172,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -189,8 +185,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId: identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
@ -293,8 +288,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit, ProjectPermissionIdentityActions.Edit,
@ -306,8 +300,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId: identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
const permissionBoundary = validatePrivilegeChangeOperation( const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem, membership.shouldUseNewPrivilegeSystem,
@ -366,8 +359,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read, ProjectPermissionIdentityActions.Read,
@ -409,8 +401,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(

View File

@ -7,7 +7,7 @@ import { KmsKeyUsage } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TKmipClientDALFactory } from "./kmip-client-dal"; import { TKmipClientDALFactory } from "./kmip-client-dal";
import { KmipPermission } from "./kmip-enum"; import { KmipPermission } from "./kmip-enum";
import { import {
@ -24,7 +24,7 @@ type TKmipOperationServiceFactoryDep = {
kmsService: TKmsServiceFactory; kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory; kmsDAL: TKmsKeyDALFactory;
kmipClientDAL: TKmipClientDALFactory; kmipClientDAL: TKmipClientDALFactory;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId" | "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
}; };

View File

@ -2,7 +2,6 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto"; import crypto, { KeyObject } from "crypto";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip"; import { isValidIp } from "@app/lib/ip";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
@ -18,7 +17,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal"; import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal";
import { TKmipClientDALFactory } from "./kmip-client-dal"; import { TKmipClientDALFactory } from "./kmip-client-dal";
@ -73,8 +72,7 @@ export const kmipServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.KMS
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -127,8 +125,7 @@ export const kmipServiceFactory = ({
actorId, actorId,
projectId: kmipClient.projectId, projectId: kmipClient.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.KMS
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -159,8 +156,7 @@ export const kmipServiceFactory = ({
actorId, actorId,
projectId: kmipClient.projectId, projectId: kmipClient.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.KMS
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -193,8 +189,7 @@ export const kmipServiceFactory = ({
actorId, actorId,
projectId: kmipClient.projectId, projectId: kmipClient.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.KMS
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
@ -215,8 +210,7 @@ export const kmipServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.KMS
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
@ -252,8 +246,7 @@ export const kmipServiceFactory = ({
actorId, actorId,
projectId: kmipClient.projectId, projectId: kmipClient.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.KMS
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(

View File

@ -29,7 +29,7 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TLdapConfigDALFactory } from "./ldap-config-dal"; import { TLdapConfigDALFactory } from "./ldap-config-dal";
import { import {
TCreateLdapCfgDTO, TCreateLdapCfgDTO,

View File

@ -18,7 +18,7 @@ import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionBillingActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums"; import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums";
import { TLicenseDALFactory } from "./license-dal"; import { TLicenseDALFactory } from "./license-dal";
import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns"; import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns";

View File

@ -5,14 +5,13 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet }
import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@ -108,34 +107,26 @@ export const oidcConfigServiceFactory = ({
kmsService kmsService
}: TOidcConfigServiceFactoryDep) => { }: TOidcConfigServiceFactoryDep) => {
const getOidc = async (dto: TGetOidcCfgDTO) => { const getOidc = async (dto: TGetOidcCfgDTO) => {
const org = await orgDAL.findOne({ slug: dto.orgSlug }); const oidcCfg = await oidcConfigDAL.findOne({
if (!org) { orgId: dto.organizationId
});
if (!oidcCfg) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${dto.orgSlug}' not found`, message: `OIDC configuration for organization with ID '${dto.organizationId}' not found`
name: "OrgNotFound"
}); });
} }
if (dto.type === "external") { if (dto.type === "external") {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
dto.actor, dto.actor,
dto.actorId, dto.actorId,
org.id, dto.organizationId,
dto.actorAuthMethod, dto.actorAuthMethod,
dto.actorOrgId dto.actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
} }
const oidcCfg = await oidcConfigDAL.findOne({
orgId: org.id
});
if (!oidcCfg) {
throw new NotFoundError({
message: `OIDC configuration for organization with slug '${dto.orgSlug}' not found`
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: oidcCfg.orgId orgId: oidcCfg.orgId
@ -466,7 +457,7 @@ export const oidcConfigServiceFactory = ({
}; };
const updateOidcCfg = async ({ const updateOidcCfg = async ({
orgSlug, organizationId,
allowedEmailDomains, allowedEmailDomains,
configurationType, configurationType,
discoveryURL, discoveryURL,
@ -485,13 +476,11 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: TUpdateOidcCfgDTO) => { }: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({ id: organizationId });
slug: orgSlug
});
if (!org) { if (!org) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${orgSlug}' not found` message: `Organization with ID '${organizationId}' not found`
}); });
} }
@ -556,7 +545,7 @@ export const oidcConfigServiceFactory = ({
}; };
const createOidcCfg = async ({ const createOidcCfg = async ({
orgSlug, organizationId,
allowedEmailDomains, allowedEmailDomains,
configurationType, configurationType,
discoveryURL, discoveryURL,
@ -575,12 +564,10 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: TCreateOidcCfgDTO) => { }: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({ id: organizationId });
slug: orgSlug
});
if (!org) { if (!org) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${orgSlug}' not found` message: `Organization with ID '${organizationId}' not found`
}); });
} }
@ -640,7 +627,7 @@ export const oidcConfigServiceFactory = ({
const oidcCfg = await getOidc({ const oidcCfg = await getOidc({
type: "internal", type: "internal",
orgSlug organizationId: org.id
}); });
if (!oidcCfg || !oidcCfg.isActive) { if (!oidcCfg || !oidcCfg.isActive) {
@ -699,9 +686,9 @@ export const oidcConfigServiceFactory = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(_req: any, tokenSet: TokenSet, cb: any) => { (_req: any, tokenSet: TokenSet, cb: any) => {
const claims = tokenSet.claims(); const claims = tokenSet.claims();
if (!claims.email || !claims.given_name) { if (!claims.email) {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid request. Missing email or first name" message: "Invalid request. Missing email claim."
}); });
} }
@ -714,12 +701,19 @@ export const oidcConfigServiceFactory = ({
} }
} }
const name = claims?.given_name || claims?.name;
if (!name) {
throw new BadRequestError({
message: "Invalid request. Missing name claim."
});
}
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined); const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({ oidcLogin({
email: claims.email.toLowerCase(), email: claims.email.toLowerCase(),
externalId: claims.sub, externalId: claims.sub,
firstName: claims.given_name ?? "", firstName: name,
lastName: claims.family_name ?? "", lastName: claims.family_name ?? "",
orgId: org.id, orgId: org.id,
groups, groups,

View File

@ -26,11 +26,11 @@ export type TOidcLoginDTO = {
export type TGetOidcCfgDTO = export type TGetOidcCfgDTO =
| ({ | ({
type: "external"; type: "external";
orgSlug: string; organizationId: string;
} & TGenericPermission) } & TGenericPermission)
| { | {
type: "internal"; type: "internal";
orgSlug: string; organizationId: string;
}; };
export type TCreateOidcCfgDTO = { export type TCreateOidcCfgDTO = {
@ -45,7 +45,7 @@ export type TCreateOidcCfgDTO = {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
} & TGenericPermission; } & TGenericPermission;
@ -62,7 +62,7 @@ export type TUpdateOidcCfgDTO = Partial<{
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
}> & }> &

View File

@ -6,16 +6,312 @@ import {
OrgMembershipRole, OrgMembershipRole,
OrgMembershipsSchema, OrgMembershipsSchema,
TableName, TableName,
TIdentityOrgMemberships,
TProjectRoles, TProjectRoles,
TProjects TProjects
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>; export interface TPermissionDALFactory {
getOrgPermission: (
userId: string,
orgId: string
) => Promise<
{
status: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
role: string;
isActive: boolean;
shouldUseNewPrivilegeSystem: boolean;
bypassOrgAuthEnabled: boolean;
permissions?: unknown;
userId?: string | null | undefined;
roleId?: string | null | undefined;
inviteEmail?: string | null | undefined;
projectFavorites?: string[] | null | undefined;
customRoleSlug?: string | null | undefined;
orgAuthEnforced?: boolean | null | undefined;
} & {
groups: {
id: string;
updatedAt: Date;
createdAt: Date;
role: string;
roleId: string | null | undefined;
customRolePermission: unknown;
name: string;
slug: string;
orgId: string;
}[];
}
>;
getOrgIdentityPermission: (
identityId: string,
orgId: string
) => Promise<
| (TIdentityOrgMemberships & {
orgAuthEnforced: boolean | null | undefined;
shouldUseNewPrivilegeSystem: boolean;
permissions?: unknown;
})
| undefined
>;
getProjectPermission: (
userId: string,
projectId: string
) => Promise<
| {
roles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
additionalPrivileges: {
id: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
orgId: string;
orgAuthEnforced: boolean | null | undefined;
orgRole: OrgMembershipRole;
userId: string;
projectId: string;
username: string;
projectType?: string | null;
id: string;
createdAt: Date;
updatedAt: Date;
shouldUseNewPrivilegeSystem: boolean;
bypassOrgAuthEnabled: boolean;
metadata: {
id: string;
key: string;
value: string;
}[];
userGroupRoles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
projecMembershiptRoles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
}
| undefined
>;
getProjectIdentityPermission: (
identityId: string,
projectId: string
) => Promise<
| {
roles: {
id: string;
createdAt: Date;
updatedAt: Date;
isTemporary: boolean;
role: string;
projectMembershipId: string;
temporaryRange?: string | null | undefined;
permissions?: unknown;
customRoleId?: string | null | undefined;
temporaryMode?: string | null | undefined;
temporaryAccessStartTime?: Date | null | undefined;
temporaryAccessEndTime?: Date | null | undefined;
customRoleSlug?: string | null | undefined;
}[];
additionalPrivileges: {
id: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
isTemporary: boolean;
}[];
id: string;
identityId: string;
username: string;
projectId: string;
createdAt: Date;
updatedAt: Date;
orgId: string;
projectType?: string | null;
shouldUseNewPrivilegeSystem: boolean;
orgAuthEnforced: boolean;
metadata: {
id: string;
key: string;
value: string;
}[];
}
| undefined
>;
getProjectUserPermissions: (projectId: string) => Promise<
{
roles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
additionalPrivileges: {
id: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
orgId: string;
orgAuthEnforced: boolean | null | undefined;
userId: string;
projectId: string;
username: string;
projectType?: string | null;
id: string;
createdAt: Date;
updatedAt: Date;
metadata: {
id: string;
key: string;
value: string;
}[];
userGroupRoles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
projectMembershipRoles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
}[]
>;
getProjectIdentityPermissions: (projectId: string) => Promise<
{
roles: {
id: string;
createdAt: Date;
updatedAt: Date;
isTemporary: boolean;
role: string;
projectMembershipId: string;
temporaryRange?: string | null | undefined;
permissions?: unknown;
customRoleId?: string | null | undefined;
temporaryMode?: string | null | undefined;
temporaryAccessStartTime?: Date | null | undefined;
temporaryAccessEndTime?: Date | null | undefined;
customRoleSlug?: string | null | undefined;
}[];
additionalPrivileges: {
id: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
isTemporary: boolean;
}[];
id: string;
identityId: string;
username: string;
projectId: string;
createdAt: Date;
updatedAt: Date;
orgId: string;
projectType?: string | null;
orgAuthEnforced: boolean;
metadata: {
id: string;
key: string;
value: string;
}[];
}[]
>;
getProjectGroupPermissions: (
projectId: string,
filterGroupId?: string
) => Promise<
{
roles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
groupId: string;
username: string;
id: string;
groupRoles: {
id: string;
role: string;
customRoleSlug: string;
permissions: unknown;
temporaryRange: string | null | undefined;
temporaryMode: string | null | undefined;
temporaryAccessStartTime: Date | null | undefined;
temporaryAccessEndTime: Date | null | undefined;
isTemporary: boolean;
}[];
}[]
>;
}
export const permissionDALFactory = (db: TDbClient) => { export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
const getOrgPermission = async (userId: string, orgId: string) => { const getOrgPermission: TPermissionDALFactory["getOrgPermission"] = async (userId: string, orgId: string) => {
try { try {
const groupSubQuery = db(TableName.Groups) const groupSubQuery = db(TableName.Groups)
.where(`${TableName.Groups}.orgId`, orgId) .where(`${TableName.Groups}.orgId`, orgId)
@ -112,7 +408,10 @@ export const permissionDALFactory = (db: TDbClient) => {
} }
}; };
const getOrgIdentityPermission = async (identityId: string, orgId: string) => { const getOrgIdentityPermission: TPermissionDALFactory["getOrgIdentityPermission"] = async (
identityId: string,
orgId: string
) => {
try { try {
const membership = await db const membership = await db
.replicaNode()(TableName.IdentityOrgMembership) .replicaNode()(TableName.IdentityOrgMembership)
@ -132,7 +431,10 @@ export const permissionDALFactory = (db: TDbClient) => {
} }
}; };
const getProjectGroupPermissions = async (projectId: string, filterGroupId?: string) => { const getProjectGroupPermissions: TPermissionDALFactory["getProjectGroupPermissions"] = async (
projectId: string,
filterGroupId?: string
) => {
try { try {
const docs = await db const docs = await db
.replicaNode()(TableName.GroupProjectMembership) .replicaNode()(TableName.GroupProjectMembership)
@ -245,7 +547,7 @@ export const permissionDALFactory = (db: TDbClient) => {
} }
}; };
const getProjectUserPermissions = async (projectId: string) => { const getProjectUserPermissions: TPermissionDALFactory["getProjectUserPermissions"] = async (projectId: string) => {
try { try {
const docs = await db const docs = await db
.replicaNode()(TableName.Users) .replicaNode()(TableName.Users)
@ -535,7 +837,10 @@ export const permissionDALFactory = (db: TDbClient) => {
} }
}; };
const getProjectPermission = async (userId: string, projectId: string) => { const getProjectPermission: TPermissionDALFactory["getProjectPermission"] = async (
userId: string,
projectId: string
) => {
try { try {
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
const docs = await db const docs = await db
@ -838,7 +1143,9 @@ export const permissionDALFactory = (db: TDbClient) => {
} }
}; };
const getProjectIdentityPermissions = async (projectId: string) => { const getProjectIdentityPermissions: TPermissionDALFactory["getProjectIdentityPermissions"] = async (
projectId: string
) => {
try { try {
const docs = await db const docs = await db
.replicaNode()(TableName.IdentityProjectMembership) .replicaNode()(TableName.IdentityProjectMembership)
@ -995,7 +1302,10 @@ export const permissionDALFactory = (db: TDbClient) => {
} }
}; };
const getProjectIdentityPermission = async (identityId: string, projectId: string) => { const getProjectIdentityPermission: TPermissionDALFactory["getProjectIdentityPermission"] = async (
identityId,
projectId
) => {
try { try {
const docs = await db const docs = await db
.replicaNode()(TableName.IdentityProjectMembership) .replicaNode()(TableName.IdentityProjectMembership)

View File

@ -1,6 +1,11 @@
import { ActionProjectType } from "@app/db/schemas"; import { MongoAbility, RawRuleOf } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { OrgPermissionSet } from "./org-permission";
import { ProjectPermissionSet } from "./project-permission";
export type TBuildProjectPermissionDTO = { export type TBuildProjectPermissionDTO = {
permissions?: unknown; permissions?: unknown;
role: string; role: string;
@ -15,7 +20,6 @@ export type TGetUserProjectPermissionArg = {
userId: string; userId: string;
projectId: string; projectId: string;
authMethod: ActorAuthMethod; authMethod: ActorAuthMethod;
actionProjectType: ActionProjectType;
userOrgId?: string; userOrgId?: string;
}; };
@ -23,14 +27,12 @@ export type TGetIdentityProjectPermissionArg = {
identityId: string; identityId: string;
projectId: string; projectId: string;
identityOrgId?: string; identityOrgId?: string;
actionProjectType: ActionProjectType;
}; };
export type TGetServiceTokenProjectPermissionArg = { export type TGetServiceTokenProjectPermissionArg = {
serviceTokenId: string; serviceTokenId: string;
projectId: string; projectId: string;
actorOrgId?: string; actorOrgId?: string;
actionProjectType: ActionProjectType;
}; };
export type TGetProjectPermissionArg = { export type TGetProjectPermissionArg = {
@ -39,5 +41,235 @@ export type TGetProjectPermissionArg = {
projectId: string; projectId: string;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId?: string; actorOrgId?: string;
actionProjectType: ActionProjectType; };
export type TPermissionServiceFactory = {
getUserOrgPermission: (
userId: string,
orgId: string,
authMethod: ActorAuthMethod,
userOrgId?: string
) => Promise<{
permission: MongoAbility<OrgPermissionSet, MongoQuery>;
membership: {
status: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
role: string;
isActive: boolean;
shouldUseNewPrivilegeSystem: boolean;
bypassOrgAuthEnabled: boolean;
permissions?: unknown;
userId?: string | null | undefined;
roleId?: string | null | undefined;
inviteEmail?: string | null | undefined;
projectFavorites?: string[] | null | undefined;
customRoleSlug?: string | null | undefined;
orgAuthEnforced?: boolean | null | undefined;
} & {
groups: {
id: string;
updatedAt: Date;
createdAt: Date;
role: string;
roleId: string | null | undefined;
customRolePermission: unknown;
name: string;
slug: string;
orgId: string;
}[];
};
}>;
getOrgPermission: (
type: ActorType,
id: string,
orgId: string,
authMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => Promise<
| {
permission: MongoAbility<OrgPermissionSet, MongoQuery>;
membership: {
status: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
role: string;
isActive: boolean;
shouldUseNewPrivilegeSystem: boolean;
bypassOrgAuthEnabled: boolean;
permissions?: unknown;
userId?: string | null | undefined;
roleId?: string | null | undefined;
inviteEmail?: string | null | undefined;
projectFavorites?: string[] | null | undefined;
customRoleSlug?: string | null | undefined;
orgAuthEnforced?: boolean | null | undefined;
} & {
groups: {
id: string;
updatedAt: Date;
createdAt: Date;
role: string;
roleId: string | null | undefined;
customRolePermission: unknown;
name: string;
slug: string;
orgId: string;
}[];
};
}
| {
permission: MongoAbility<OrgPermissionSet, MongoQuery>;
membership: {
id: string;
role: string;
createdAt: Date;
updatedAt: Date;
orgId: string;
roleId?: string | null | undefined;
permissions?: unknown;
identityId: string;
orgAuthEnforced: boolean | null | undefined;
shouldUseNewPrivilegeSystem: boolean;
};
}
>;
getUserProjectPermission: ({ userId, projectId, authMethod, userOrgId }: TGetUserProjectPermissionArg) => Promise<{
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: {
id: string;
createdAt: Date;
updatedAt: Date;
userId: string;
projectId: string;
} & {
orgAuthEnforced: boolean | null | undefined;
orgId: string;
roles: Array<{
role: string;
}>;
shouldUseNewPrivilegeSystem: boolean;
};
hasRole: (role: string) => boolean;
}>;
getProjectPermission: <T extends ActorType>(
arg: TGetProjectPermissionArg
) => Promise<
T extends ActorType.SERVICE
? {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: {
shouldUseNewPrivilegeSystem: boolean;
};
hasRole: (arg: string) => boolean;
}
: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: (T extends ActorType.USER
? {
id: string;
createdAt: Date;
updatedAt: Date;
userId: string;
projectId: string;
}
: {
id: string;
createdAt: Date;
updatedAt: Date;
projectId: string;
identityId: string;
}) & {
orgAuthEnforced: boolean | null | undefined;
orgId: string;
roles: Array<{
role: string;
}>;
shouldUseNewPrivilegeSystem: boolean;
};
hasRole: (role: string) => boolean;
}
>;
getProjectPermissions: (projectId: string) => Promise<{
userPermissions: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
id: string;
name: string;
membershipId: string;
}[];
identityPermissions: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
id: string;
name: string;
membershipId: string;
}[];
groupPermissions: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
id: string;
name: string;
membershipId: string;
}[];
}>;
getOrgPermissionByRole: (
role: string,
orgId: string
) => Promise<
| {
permission: MongoAbility<OrgPermissionSet, MongoQuery>;
role: {
name: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
slug: string;
permissions?: unknown;
description?: string | null | undefined;
};
}
| {
permission: MongoAbility<OrgPermissionSet, MongoQuery>;
role?: undefined;
}
>;
getProjectPermissionByRole: (
role: string,
projectId: string
) => Promise<
| {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
role: {
name: string;
version: number;
id: string;
createdAt: Date;
updatedAt: Date;
projectId: string;
slug: string;
permissions?: unknown;
description?: string | null | undefined;
};
}
| {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
role?: undefined;
}
>;
buildOrgPermission: (orgUserRoles: TBuildOrgPermissionDTO) => MongoAbility<OrgPermissionSet, MongoQuery>;
buildProjectPermissionRules: (
projectUserRoles: TBuildProjectPermissionDTO
) => RawRuleOf<MongoAbility<ProjectPermissionSet>>[];
checkGroupProjectPermission: ({
groupId,
projectId,
checkPermissions
}: {
groupId: string;
projectId: string;
checkPermissions: ProjectPermissionSet;
}) => Promise<boolean>;
}; };

View File

@ -5,7 +5,6 @@ import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars"; import handlebars from "handlebars";
import { import {
ActionProjectType,
OrgMembershipRole, OrgMembershipRole,
ProjectMembershipRole, ProjectMembershipRole,
ServiceTokenScopes, ServiceTokenScopes,
@ -23,7 +22,7 @@ import {
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn"; import { objectify } from "@app/lib/fn";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal";
@ -38,7 +37,8 @@ import {
TGetIdentityProjectPermissionArg, TGetIdentityProjectPermissionArg,
TGetProjectPermissionArg, TGetProjectPermissionArg,
TGetServiceTokenProjectPermissionArg, TGetServiceTokenProjectPermissionArg,
TGetUserProjectPermissionArg TGetUserProjectPermissionArg,
TPermissionServiceFactory
} from "./permission-service-types"; } from "./permission-service-types";
import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission"; import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission";
@ -50,15 +50,13 @@ type TPermissionServiceFactoryDep = {
permissionDAL: TPermissionDALFactory; permissionDAL: TPermissionDALFactory;
}; };
export type TPermissionServiceFactory = ReturnType<typeof permissionServiceFactory>;
export const permissionServiceFactory = ({ export const permissionServiceFactory = ({
permissionDAL, permissionDAL,
orgRoleDAL, orgRoleDAL,
projectRoleDAL, projectRoleDAL,
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL
}: TPermissionServiceFactoryDep) => { }: TPermissionServiceFactoryDep): TPermissionServiceFactory => {
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
const rules = orgUserRoles const rules = orgUserRoles
.map(({ role, permissions }) => { .map(({ role, permissions }) => {
@ -120,11 +118,11 @@ export const permissionServiceFactory = ({
/* /*
* Get user permission in an organization * Get user permission in an organization
*/ */
const getUserOrgPermission = async ( const getUserOrgPermission: TPermissionServiceFactory["getUserOrgPermission"] = async (
userId: string, userId,
orgId: string, orgId,
authMethod: ActorAuthMethod, authMethod,
userOrgId?: string userOrgId
) => { ) => {
// when token is scoped, ensure the passed org id is same as user org id // when token is scoped, ensure the passed org id is same as user org id
if (userOrgId && userOrgId !== orgId) if (userOrgId && userOrgId !== orgId)
@ -172,12 +170,12 @@ export const permissionServiceFactory = ({
}; };
}; };
const getOrgPermission = async ( const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async (
type: ActorType, type,
id: string, id,
orgId: string, orgId,
authMethod: ActorAuthMethod, authMethod,
actorOrgId: string | undefined actorOrgId
) => { ) => {
switch (type) { switch (type) {
case ActorType.USER: case ActorType.USER:
@ -194,7 +192,7 @@ export const permissionServiceFactory = ({
// instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like // instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like
// admin member or user defined ones like biller etc // admin member or user defined ones like biller etc
const getOrgPermissionByRole = async (role: string, orgId: string) => { const getOrgPermissionByRole: TPermissionServiceFactory["getOrgPermissionByRole"] = async (role, orgId) => {
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
if (isCustomRole) { if (isCustomRole) {
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
@ -215,8 +213,7 @@ export const permissionServiceFactory = ({
userId, userId,
projectId, projectId,
authMethod, authMethod,
userOrgId, userOrgId
actionProjectType
}: TGetUserProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.USER>> => { }: TGetUserProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.USER>> => {
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" }); if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" });
@ -243,12 +240,6 @@ export const permissionServiceFactory = ({
userProjectPermission.orgRole userProjectPermission.orgRole
); );
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.`
});
}
// join two permissions and pass to build the final permission set // join two permissions and pass to build the final permission set
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges = const additionalPrivileges =
@ -296,8 +287,7 @@ export const permissionServiceFactory = ({
const getIdentityProjectPermission = async ({ const getIdentityProjectPermission = async ({
identityId, identityId,
projectId, projectId,
identityOrgId, identityOrgId
actionProjectType
}: TGetIdentityProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => { }: TGetIdentityProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!identityProjectPermission) if (!identityProjectPermission)
@ -317,12 +307,6 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" }); throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" });
} }
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.`
});
}
const rolePermissions = const rolePermissions =
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges = const additionalPrivileges =
@ -377,8 +361,7 @@ export const permissionServiceFactory = ({
const getServiceTokenProjectPermission = async ({ const getServiceTokenProjectPermission = async ({
serviceTokenId, serviceTokenId,
projectId, projectId,
actorOrgId, actorOrgId
actionProjectType
}: TGetServiceTokenProjectPermissionArg) => { }: TGetServiceTokenProjectPermissionArg) => {
const serviceToken = await serviceTokenDAL.findById(serviceTokenId); const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${serviceTokenId}' not found` }); if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${serviceTokenId}' not found` });
@ -403,12 +386,6 @@ export const permissionServiceFactory = ({
}); });
} }
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== serviceTokenProject.type) {
throw new BadRequestError({
message: `The project is of type ${serviceTokenProject.type}. Operations of type ${actionProjectType} are not allowed.`
});
}
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return { return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
@ -437,7 +414,7 @@ export const permissionServiceFactory = ({
hasRole: (role: string) => boolean; hasRole: (role: string) => boolean;
}; };
const getProjectPermissions = async (projectId: string) => { const getProjectPermissions: TPermissionServiceFactory["getProjectPermissions"] = async (projectId) => {
// fetch user permissions // fetch user permissions
const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId); const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId);
const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => { const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => {
@ -560,8 +537,7 @@ export const permissionServiceFactory = ({
actorId: inputActorId, actorId: inputActorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => { }: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
let actor = inputActor; let actor = inputActor;
let actorId = inputActorId; let actorId = inputActorId;
@ -582,22 +558,19 @@ export const permissionServiceFactory = ({
userId: actorId, userId: actorId,
projectId, projectId,
authMethod: actorAuthMethod, authMethod: actorAuthMethod,
userOrgId: actorOrgId, userOrgId: actorOrgId
actionProjectType
}) as Promise<TProjectPermissionRT<T>>; }) as Promise<TProjectPermissionRT<T>>;
case ActorType.SERVICE: case ActorType.SERVICE:
return getServiceTokenProjectPermission({ return getServiceTokenProjectPermission({
serviceTokenId: actorId, serviceTokenId: actorId,
projectId, projectId,
actorOrgId, actorOrgId
actionProjectType
}) as Promise<TProjectPermissionRT<T>>; }) as Promise<TProjectPermissionRT<T>>;
case ActorType.IDENTITY: case ActorType.IDENTITY:
return getIdentityProjectPermission({ return getIdentityProjectPermission({
identityId: actorId, identityId: actorId,
projectId, projectId,
identityOrgId: actorOrgId, identityOrgId: actorOrgId
actionProjectType
}) as Promise<TProjectPermissionRT<T>>; }) as Promise<TProjectPermissionRT<T>>;
default: default:
throw new BadRequestError({ throw new BadRequestError({
@ -607,7 +580,10 @@ export const permissionServiceFactory = ({
} }
}; };
const getProjectPermissionByRole = async (role: string, projectId: string) => { const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async (
role,
projectId
) => {
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
if (isCustomRole) { if (isCustomRole) {
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
@ -630,14 +606,10 @@ export const permissionServiceFactory = ({
return { permission }; return { permission };
}; };
const checkGroupProjectPermission = async ({ const checkGroupProjectPermission: TPermissionServiceFactory["checkGroupProjectPermission"] = async ({
groupId, groupId,
projectId, projectId,
checkPermissions checkPermissions
}: {
groupId: string;
projectId: string;
checkPermissions: ProjectPermissionSet;
}) => { }) => {
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId, groupId); const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId, groupId);
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => { const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {

View File

@ -211,6 +211,11 @@ export type SecretFolderSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type SecretSyncSubjectFields = {
environment: string;
secretPath: string;
};
export type DynamicSecretSubjectFields = { export type DynamicSecretSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@ -267,6 +272,10 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
) )
] ]
| [
ProjectPermissionSecretSyncActions,
ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
( (
@ -323,7 +332,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
@ -412,6 +420,23 @@ const DynamicSecretConditionV2Schema = z
}) })
.partial(); .partial();
const SecretSyncConditionV2Schema = z
.object({
environment: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
})
.partial();
const SecretImportConditionSchema = z const SecretImportConditionSchema = z
.object({ .object({
environment: z.union([ environment: z.union([
@ -671,12 +696,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
@ -836,6 +855,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
),
conditions: SecretSyncConditionV2Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);

View File

@ -1,7 +1,6 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -16,7 +15,7 @@ import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
type TPitServiceFactoryDep = { type TPitServiceFactoryDep = {
folderCommitService: TFolderCommitServiceFactory; folderCommitService: TFolderCommitServiceFactory;
@ -321,8 +320,7 @@ export const pitServiceFactory = ({
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(userPermission).throwUnlessCan( ForbiddenError.from(userPermission).throwUnlessCan(

View File

@ -1,7 +1,8 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { ormify, TOrmify } from "@app/lib/knex";
export type TProjectTemplateDALFactory = ReturnType<typeof projectTemplateDALFactory>; export type TProjectTemplateDALFactory = TOrmify<TableName.ProjectTemplates>;
export const projectTemplateDALFactory = (db: TDbClient) => ormify(db, TableName.ProjectTemplates); export const projectTemplateDALFactory = (db: TDbClient): TProjectTemplateDALFactory =>
ormify(db, TableName.ProjectTemplates);

View File

@ -1,4 +1,3 @@
import { ProjectType } from "@app/db/schemas";
import { import {
InfisicalProjectTemplate, InfisicalProjectTemplate,
TUnpackedPermission TUnpackedPermission
@ -7,21 +6,18 @@ import { getPredefinedRoles } from "@app/services/project-role/project-role-fns"
import { ProjectTemplateDefaultEnvironments } from "./project-template-constants"; import { ProjectTemplateDefaultEnvironments } from "./project-template-constants";
export const getDefaultProjectTemplate = (orgId: string, type: ProjectType) => ({ export const getDefaultProjectTemplate = (orgId: string) => ({
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
type,
name: InfisicalProjectTemplate.Default, name: InfisicalProjectTemplate.Default,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
description: `Infisical's ${type} default project template`, description: `Infisical's default project template`,
environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null, environments: ProjectTemplateDefaultEnvironments,
roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map( roles: getPredefinedRoles({ projectId: "project-template" }) as Array<{
({ name, slug, permissions }) => ({ name: string;
name, slug: string;
slug, permissions: TUnpackedPermission[];
permissions: permissions as TUnpackedPermission[] }>,
})
),
orgId orgId
}); });

View File

@ -1,21 +1,19 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
import { ProjectType, TProjectTemplates } from "@app/db/schemas"; import { TProjectTemplates } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { import {
TCreateProjectTemplateDTO,
TProjectTemplateEnvironment, TProjectTemplateEnvironment,
TProjectTemplateRole, TProjectTemplateRole,
TUnpackedPermission, TProjectTemplateServiceFactory,
TUpdateProjectTemplateDTO TUnpackedPermission
} from "@app/ee/services/project-template/project-template-types"; } from "@app/ee/services/project-template/project-template-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns"; import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
@ -27,19 +25,15 @@ type TProjectTemplatesServiceFactoryDep = {
projectTemplateDAL: TProjectTemplateDALFactory; projectTemplateDAL: TProjectTemplateDALFactory;
}; };
export type TProjectTemplateServiceFactory = ReturnType<typeof projectTemplateServiceFactory>;
const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTemplates) => ({ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTemplates) => ({
...rest, ...rest,
environments: environments as TProjectTemplateEnvironment[], environments: environments as TProjectTemplateEnvironment[],
roles: [ roles: [
...getPredefinedRoles({ projectId: "project-template", projectType: rest.type as ProjectType }).map( ...getPredefinedRoles({ projectId: "project-template" }).map(({ name, slug, permissions }) => ({
({ name, slug, permissions }) => ({ name,
name, slug,
slug, permissions: permissions as TUnpackedPermission[]
permissions: permissions as TUnpackedPermission[] })),
})
),
...(roles as TProjectTemplateRole[]).map((role) => ({ ...(roles as TProjectTemplateRole[]).map((role) => ({
...role, ...role,
permissions: unpackPermissions(role.permissions) permissions: unpackPermissions(role.permissions)
@ -51,8 +45,8 @@ export const projectTemplateServiceFactory = ({
licenseService, licenseService,
permissionService, permissionService,
projectTemplateDAL projectTemplateDAL
}: TProjectTemplatesServiceFactoryDep) => { }: TProjectTemplatesServiceFactoryDep): TProjectTemplateServiceFactory => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => { const listProjectTemplatesByOrg: TProjectTemplateServiceFactory["listProjectTemplatesByOrg"] = async (actor) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates) if (!plan.projectTemplates)
@ -71,19 +65,19 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
const projectTemplates = await projectTemplateDAL.find({ const projectTemplates = await projectTemplateDAL.find({
orgId: actor.orgId, orgId: actor.orgId
...(type ? { type } : {})
}); });
return [ return [
...(type getDefaultProjectTemplate(actor.orgId),
? [getDefaultProjectTemplate(actor.orgId, type)]
: Object.values(ProjectType).map((projectType) => getDefaultProjectTemplate(actor.orgId, projectType))),
...projectTemplates.map((template) => $unpackProjectTemplate(template)) ...projectTemplates.map((template) => $unpackProjectTemplate(template))
]; ];
}; };
const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => { const findProjectTemplateByName: TProjectTemplateServiceFactory["findProjectTemplateByName"] = async (
name,
actor
) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates) if (!plan.projectTemplates)
@ -111,7 +105,7 @@ export const projectTemplateServiceFactory = ({
}; };
}; };
const findProjectTemplateById = async (id: string, actor: OrgServiceActor) => { const findProjectTemplateById: TProjectTemplateServiceFactory["findProjectTemplateById"] = async (id, actor) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates) if (!plan.projectTemplates)
@ -139,9 +133,9 @@ export const projectTemplateServiceFactory = ({
}; };
}; };
const createProjectTemplate = async ( const createProjectTemplate: TProjectTemplateServiceFactory["createProjectTemplate"] = async (
{ roles, environments, type, ...params }: TCreateProjectTemplateDTO, { roles, environments, ...params },
actor: OrgServiceActor actor
) => { ) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
@ -160,10 +154,6 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
if (environments && type !== ProjectType.SecretManager) {
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
}
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) { if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
throw new BadRequestError({ throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
@ -186,19 +176,17 @@ export const projectTemplateServiceFactory = ({
const projectTemplate = await projectTemplateDAL.create({ const projectTemplate = await projectTemplateDAL.create({
...params, ...params,
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))), roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
environments: environments: environments ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null, orgId: actor.orgId
orgId: actor.orgId,
type
}); });
return $unpackProjectTemplate(projectTemplate); return $unpackProjectTemplate(projectTemplate);
}; };
const updateProjectTemplateById = async ( const updateProjectTemplateById: TProjectTemplateServiceFactory["updateProjectTemplateById"] = async (
id: string, id,
{ roles, environments, ...params }: TUpdateProjectTemplateDTO, { roles, environments, ...params },
actor: OrgServiceActor actor
) => { ) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
@ -221,12 +209,6 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
if (projectTemplate.type !== ProjectType.SecretManager && environments)
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
if (projectTemplate.type === ProjectType.SecretManager && environments === null)
throw new BadRequestError({ message: "Environments cannot be removed for SecretManager project templates" });
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) { if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
throw new BadRequestError({ throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
@ -259,7 +241,7 @@ export const projectTemplateServiceFactory = ({
return $unpackProjectTemplate(updatedProjectTemplate); return $unpackProjectTemplate(updatedProjectTemplate);
}; };
const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => { const deleteProjectTemplateById: TProjectTemplateServiceFactory["deleteProjectTemplateById"] = async (id, actor) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates) if (!plan.projectTemplates)

View File

@ -1,7 +1,8 @@
import { z } from "zod"; import { z } from "zod";
import { ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { ProjectMembershipRole, TProjectEnvironments } from "@app/db/schemas";
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { OrgServiceActor } from "@app/lib/types";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">; export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">;
@ -15,7 +16,6 @@ export type TProjectTemplateRole = {
export type TCreateProjectTemplateDTO = { export type TCreateProjectTemplateDTO = {
name: string; name: string;
description?: string; description?: string;
type: ProjectType;
roles: TProjectTemplateRole[]; roles: TProjectTemplateRole[];
environments?: TProjectTemplateEnvironment[] | null; environments?: TProjectTemplateEnvironment[] | null;
}; };
@ -27,3 +27,167 @@ export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>;
export enum InfisicalProjectTemplate { export enum InfisicalProjectTemplate {
Default = "default" Default = "default"
} }
export type TProjectTemplateServiceFactory = {
listProjectTemplatesByOrg: (actor: OrgServiceActor) => Promise<
(
| {
id: string;
name: InfisicalProjectTemplate;
createdAt: Date;
updatedAt: Date;
description: string;
environments:
| {
name: string;
slug: string;
position: number;
}[]
| null;
roles: {
name: string;
slug: ProjectMembershipRole;
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
}[];
orgId: string;
}
| {
environments: TProjectTemplateEnvironment[];
roles: {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
slug: string;
name: string;
}[];
name: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
description?: string | null | undefined;
}
)[]
>;
createProjectTemplate: (
arg: TCreateProjectTemplateDTO,
actor: OrgServiceActor
) => Promise<{
environments: TProjectTemplateEnvironment[];
roles: {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
slug: string;
name: string;
}[];
name: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
description?: string | null | undefined;
}>;
updateProjectTemplateById: (
id: string,
{ roles, environments, ...params }: TUpdateProjectTemplateDTO,
actor: OrgServiceActor
) => Promise<{
environments: TProjectTemplateEnvironment[];
roles: {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
slug: string;
name: string;
}[];
name: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
description?: string | null | undefined;
}>;
deleteProjectTemplateById: (
id: string,
actor: OrgServiceActor
) => Promise<{
environments: TProjectTemplateEnvironment[];
roles: {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
slug: string;
name: string;
}[];
name: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
description?: string | null | undefined;
}>;
findProjectTemplateById: (
id: string,
actor: OrgServiceActor
) => Promise<{
packedRoles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[];
roles: {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
slug: string;
name: string;
}[];
name: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
description?: string | null | undefined;
}>;
findProjectTemplateByName: (
name: string,
actor: OrgServiceActor
) => Promise<{
packedRoles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[];
roles: {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
slug: string;
name: string;
}[];
name: string;
orgId: string;
id: string;
createdAt: Date;
updatedAt: Date;
description?: string | null | undefined;
}>;
};

View File

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

View File

@ -1,7 +1,7 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@ -11,7 +11,7 @@ import { TProjectMembershipDALFactory } from "@app/services/project-membership/p
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal"; import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { import {
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionSet, ProjectPermissionSet,
@ -21,11 +21,7 @@ import { ApprovalStatus } from "../secret-approval-request/secret-approval-reque
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
import { import {
ProjectUserAdditionalPrivilegeTemporaryMode, ProjectUserAdditionalPrivilegeTemporaryMode,
TCreateUserPrivilegeDTO, TProjectUserAdditionalPrivilegeServiceFactory
TDeleteUserPrivilegeDTO,
TGetUserPrivilegeDetailsDTO,
TListUserPrivilegesDTO,
TUpdateUserPrivilegeDTO
} from "./project-user-additional-privilege-types"; } from "./project-user-additional-privilege-types";
type TProjectUserAdditionalPrivilegeServiceFactoryDep = { type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
@ -35,10 +31,6 @@ type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">; accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">;
}; };
export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
typeof projectUserAdditionalPrivilegeServiceFactory
>;
const unpackPermissions = (permissions: unknown) => const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse( UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]) unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
@ -49,8 +41,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
projectMembershipDAL, projectMembershipDAL,
permissionService, permissionService,
accessApprovalRequestDAL accessApprovalRequestDAL
}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => { }: TProjectUserAdditionalPrivilegeServiceFactoryDep): TProjectUserAdditionalPrivilegeServiceFactory => {
const create = async ({ const create: TProjectUserAdditionalPrivilegeServiceFactory["create"] = async ({
slug, slug,
actor, actor,
actorId, actorId,
@ -59,7 +51,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
projectMembershipId, projectMembershipId,
...dto ...dto
}: TCreateUserPrivilegeDTO) => { }) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId); const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership) if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` }); throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` });
@ -69,8 +61,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({ const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({
@ -78,8 +69,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorId: projectMembership.userId, actorId: projectMembership.userId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
@ -147,14 +137,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}; };
}; };
const updateById = async ({ const updateById: TProjectUserAdditionalPrivilegeServiceFactory["updateById"] = async ({
privilegeId, privilegeId,
actorOrgId, actorOrgId,
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
...dto ...dto
}: TUpdateUserPrivilegeDTO) => { }) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
@ -174,8 +164,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission({ const { permission: targetUserPermission } = await permissionService.getProjectPermission({
@ -183,8 +172,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorId: projectMembership.userId, actorId: projectMembership.userId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
@ -259,7 +247,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}; };
}; };
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { const deleteById: TProjectUserAdditionalPrivilegeServiceFactory["deleteById"] = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
privilegeId
}) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
@ -278,8 +272,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
@ -299,13 +292,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}; };
}; };
const getPrivilegeDetailsById = async ({ const getPrivilegeDetailsById: TProjectUserAdditionalPrivilegeServiceFactory["getPrivilegeDetailsById"] = async ({
privilegeId, privilegeId,
actorOrgId, actorOrgId,
actor, actor,
actorId, actorId,
actorAuthMethod actorAuthMethod
}: TGetUserPrivilegeDetailsDTO) => { }) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
@ -324,8 +317,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
@ -335,13 +327,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
}; };
}; };
const listPrivileges = async ({ const listPrivileges: TProjectUserAdditionalPrivilegeServiceFactory["listPrivileges"] = async ({
projectMembershipId, projectMembershipId,
actorOrgId, actorOrgId,
actor, actor,
actorId, actorId,
actorAuthMethod actorAuthMethod
}: TListUserPrivilegesDTO) => { }) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId); const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership) if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` }); throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` });
@ -351,8 +343,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorId, actorId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);

View File

@ -1,3 +1,4 @@
import { TProjectUserAdditionalPrivilege } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TProjectPermissionV2Schema } from "../permission/project-permission"; import { TProjectPermissionV2Schema } from "../permission/project-permission";
@ -40,3 +41,20 @@ export type TDeleteUserPrivilegeDTO = Omit<TProjectPermission, "projectId"> & {
export type TGetUserPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string }; export type TGetUserPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
export type TListUserPrivilegesDTO = Omit<TProjectPermission, "projectId"> & { projectMembershipId: string }; export type TListUserPrivilegesDTO = Omit<TProjectPermission, "projectId"> & { projectMembershipId: string };
interface TAdditionalPrivilege extends TProjectUserAdditionalPrivilege {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
}
export type TProjectUserAdditionalPrivilegeServiceFactory = {
create: (arg: TCreateUserPrivilegeDTO) => Promise<TAdditionalPrivilege>;
updateById: (arg: TUpdateUserPrivilegeDTO) => Promise<TAdditionalPrivilege>;
deleteById: (arg: TDeleteUserPrivilegeDTO) => Promise<TAdditionalPrivilege>;
getPrivilegeDetailsById: (arg: TGetUserPrivilegeDetailsDTO) => Promise<TAdditionalPrivilege>;
listPrivileges: (arg: TListUserPrivilegesDTO) => Promise<TProjectUserAdditionalPrivilege[]>;
};

View File

@ -1,7 +1,7 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { ormify, TOrmify } from "@app/lib/knex";
export type TRateLimitDALFactory = ReturnType<typeof rateLimitDALFactory>; export type TRateLimitDALFactory = TOrmify<TableName.RateLimit>;
export const rateLimitDALFactory = (db: TDbClient) => ormify(db, TableName.RateLimit, {}); export const rateLimitDALFactory = (db: TDbClient): TRateLimitDALFactory => ormify(db, TableName.RateLimit, {});

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