Compare commits

...

183 Commits

Author SHA1 Message Date
Daniel Hougaard
074446df1f Update agent.go 2025-05-20 14:32:07 +04:00
Maidul Islam
0b6bc4c1f0 update spend 2025-05-19 21:58:19 -07:00
Maidul Islam
abbe7bbd0c Merge pull request #3627 from Infisical/fix-breaking-schema-changes--for-k8s
Allow Hyphens in k8s
2025-05-19 18:26:09 -07:00
Maidul Islam
565340dc50 fix lint 2025-05-19 18:13:45 -07:00
Maidul Islam
36c428f152 allow hyphens in host name 2025-05-19 17:45:12 -07:00
Maidul Islam
f97826ea82 allow hyphens in host name 2025-05-19 17:42:42 -07:00
Maidul Islam
0f5cbf055c remove limit 2025-05-19 17:27:47 -07:00
x032205
b960ee61d7 Merge pull request #3624 from Infisical/product-select-docs
add product select to docs + change the heading
2025-05-19 17:16:38 -04:00
x032205
0b98a214a7 ui tweaks 2025-05-19 17:15:42 -04:00
x032205
599c2226e4 Merge pull request #3615 from Infisical/ENG-2787
feat(org): Shared Secret limits for org
2025-05-19 16:26:10 -04:00
x032205
27486e7600 Merge pull request #3625 from Infisical/ENG-2795
fix secret rollback not tainting form
2025-05-19 16:17:26 -04:00
x032205
979e9efbcb fix lint issue 2025-05-19 15:52:50 -04:00
x032205
1097ec64b2 ui improvements 2025-05-19 15:40:07 -04:00
x032205
93fe9929b7 fix secret rollback not tainting form 2025-05-19 15:22:24 -04:00
x032205
aca654a993 Update docs/documentation/platform/organization.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-19 13:38:34 -04:00
x032205
b5cf237a4a add product select to docs + change the heading 2025-05-19 13:35:35 -04:00
x032205
6efb630200 Moved secret share limits to secret share settings 2025-05-19 12:32:22 -04:00
x032205
151ede6cbf Merge 2025-05-19 12:20:02 -04:00
x032205
931ee1e8da Merge pull request #3616 from Infisical/ENG-2783
feat(secret-sharing): Specify Emails
2025-05-19 12:12:07 -04:00
x032205
0401793d38 Changed "token" param to "hash" and used hex encoding for URL 2025-05-19 10:48:58 -04:00
x032205
0613c12508 Merge pull request #3618 from Infisical/fix-bundle-for-old-certs 2025-05-18 13:29:31 -04:00
Daniel Hougaard
60d3ffac5d Merge pull request #3620 from Infisical/daniel/k8s-auth-fix
fix(identities-auth): fixed kubernetes auth login
2025-05-17 22:18:52 +04:00
Daniel Hougaard
5e192539a1 Update identity-kubernetes-auth-service.ts 2025-05-17 22:13:49 +04:00
Daniel Hougaard
021a8ddace Update identity-kubernetes-auth-service.ts 2025-05-17 22:06:51 +04:00
x032205
f92aba14cd Merge pull request #3619 from Infisical/fix-padding
Org Products Padding Fix
2025-05-17 13:11:56 -04:00
x032205
fdeefcdfcf padding to match similar container 2025-05-17 13:10:15 -04:00
x032205
645f70f770 tweaks 2025-05-17 13:05:09 -04:00
x032205
923feb81f3 fix bundle endpoint for old certs 2025-05-17 12:44:05 -04:00
x032205
16c51af340 review fixes 2025-05-17 02:17:41 -04:00
x032205
9fd37ca456 greptile review fixes 2025-05-17 01:51:05 -04:00
x032205
92bebf7d84 feat(secret-sharing): Specify Emails 2025-05-17 00:54:40 -04:00
x032205
df053bbae9 Merge pull request #3611 from Infisical/ENG-2782
feat(project): Enable / Disable Secret Sharing
2025-05-16 18:58:39 -04:00
x032205
42319f01a7 greptile review fixes 2025-05-16 18:54:57 -04:00
x032205
0ea9f9b60d feat(org): Shared Secret limits for org 2025-05-16 18:36:02 -04:00
Scott Wilson
16eefe5bac Merge pull request #3610 from Infisical/sso-empty-state
improvement(sso-page): Add empty display for SSO general tab if no SSO is enabled
2025-05-16 10:10:16 -07:00
Daniel Hougaard
b984111a73 Merge pull request #3612 from Infisical/daniel/cli-auth-fix
fix(auth): cli auth bug
2025-05-16 17:29:21 +04:00
Daniel Hougaard
677ff62b5c fix(auth): cli auth bug 2025-05-16 17:22:18 +04:00
Daniel Hougaard
8cc2e08f24 fix(auth): cli auth bug 2025-05-16 16:58:01 +04:00
Maidul Islam
d90178f49a Merge pull request #3590 from Infisical/daniel/k8s-auth-gateway
feat(gateway): gateway support for identities
2025-05-16 00:10:16 -07:00
x032205
ad50cff184 Update frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-16 00:21:30 -04:00
x032205
8e43d2a994 feat(project): Enable / Disable Secret Sharing 2025-05-16 00:08:55 -04:00
x032205
7074fdbac3 Merge pull request #3609 from Infisical/ENG-2736
feat(org-settings): Option to hide certain products from the sidebar
2025-05-15 23:24:14 -04:00
Scott Wilson
ef70de1e0b fix: add noopenner to doc link 2025-05-15 20:05:56 -07:00
Scott Wilson
7e9ee7b5e3 fix: add empty display for sso general tab if no sso is enabled 2025-05-15 20:01:08 -07:00
x032205
517c613d05 migration fix 2025-05-15 22:50:09 -04:00
x032205
ae8cf06ec6 greptile review fixes 2025-05-15 21:05:39 -04:00
x032205
818778ddc5 Update frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-15 21:01:46 -04:00
x032205
2e12d9a13c Update frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-15 21:01:30 -04:00
x032205
e678c9d1cf remove comments 2025-05-15 20:49:01 -04:00
x032205
da0b07ce2a added the other two products and small UI tweaks 2025-05-15 20:45:32 -04:00
x032205
3306a9ca69 Merge pull request #3608 from Infisical/key-schema-tweak
allow underscores in key schema
2025-05-15 18:55:45 -04:00
Maidul Islam
e9af34a6ba Merge pull request #3607 from Infisical/key-schema-doc-tweaks
feat(docs): Key Schema Tweaks
2025-05-15 15:51:23 -07:00
x032205
3de8ed169f allow underscores in key schema 2025-05-15 18:49:30 -04:00
Scott Wilson
d1eb350bdd Merge pull request #3606 from Infisical/oidc-groups-claim-handle-string
improvement(oidc-group-membership-mapping): Update OIDC group claims to handle single group string
2025-05-15 14:47:46 -07:00
Scott Wilson
0c1ccf7c2e fix: update oidc group claims to handle single group string 2025-05-15 14:39:07 -07:00
x032205
d268f52a1c small ui tweak 2025-05-15 16:50:37 -04:00
x032205
c519cee5d1 frontend 2025-05-15 16:32:57 -04:00
Maidul Islam
b55a39dd24 Merge pull request #3604 from Infisical/misc/add-identity-support-for-audit-log-retention
misc: add identity support for audit log retention
2025-05-15 09:25:49 -07:00
Sheen
7b880f85cc misc: add identity support for audit log retention 2025-05-15 16:19:47 +00:00
x032205
c7dc595e1a doc overview update 2025-05-15 12:05:06 -04:00
x032205
6e494f198b Merge pull request #3603 from Infisical/fix-oci-machine-identity
fix oci machine identity
2025-05-15 11:42:58 -04:00
x032205
e1f3eaf1a0 Comment for regex 2025-05-15 11:41:00 -04:00
Daniel Hougaard
be26dc9872 requested changes 2025-05-15 16:55:36 +04:00
Daniel Hougaard
aaeb6e73fe requested changes 2025-05-15 16:06:20 +04:00
x032205
1e11702c58 remove unused import 2025-05-15 01:17:38 -04:00
x032205
3b81cdb16e fix oci machine identity 2025-05-15 01:12:33 -04:00
x032205
6584166815 Merge pull request #3598 from Infisical/ENG-2755
feat(secret-sync): Secret Key Schema
2025-05-14 23:57:18 -04:00
x032205
827cb35194 review fixes 2025-05-14 23:52:05 -04:00
Maidul Islam
89a6a0ba13 Merge pull request #3602 from Infisical/general-oidc-group-mapping-docs
docs(oidc-group-membership-mapping): Add general OIDC group membership mapping documentation
2025-05-14 16:25:26 -07:00
Scott Wilson
3b9a50d65d improvements: address feedback 2025-05-14 16:20:50 -07:00
Scott Wilson
beb7200233 fix: correct overview image links 2025-05-14 14:29:46 -07:00
Scott Wilson
18e3d132a2 documentation: add general oidc group membership mapping documentation 2025-05-14 14:22:35 -07:00
x032205
3f74d3a80d update import 2025-05-14 13:49:25 -04:00
x032205
4a44dc6119 format a frontend file 2025-05-14 13:45:45 -04:00
x032205
dd4bc4bc73 more doc tweaks 2025-05-14 13:43:23 -04:00
x032205
6188de43e4 Merge pull request #3574 from Infisical/ENG-2706
feat(machine-identities): oracle cloud machine identity auth
2025-05-14 12:56:16 -04:00
Daniel Hougaard
36310387e0 Update oci-auth.mdx 2025-05-14 20:44:41 +04:00
x032205
43f3960225 Merge branch 'main' into ENG-2706 2025-05-14 12:35:17 -04:00
Scott Wilson
2f0a442866 Merge pull request #3573 from Infisical/duplicate-project-roles
feature(project/org-roles): Add ability to duplicate org and project roles
2025-05-14 09:23:02 -07:00
Scott Wilson
7e05bc86a9 improvement: address feedback 2025-05-14 08:58:29 -07:00
x032205
b0c4fddf86 review fixes 2025-05-14 11:23:12 -04:00
Maidul Islam
f5578d39a6 Merge pull request #3597 from Infisical/linux-upgrade-docs
add linux upgrade docs
2025-05-14 07:45:01 -07:00
Daniel Hougaard
cd028ae133 Update 20250212191958_create-gateway.ts 2025-05-14 16:01:07 +04:00
Daniel Hougaard
63c71fabcd fix: migrate project gateway 2025-05-14 16:00:27 +04:00
Daniel Hougaard
e90166f1f0 Merge branch 'heads/main' into daniel/k8s-auth-gateway 2025-05-14 14:26:05 +04:00
Sheen
5a3fbc0401 Merge pull request #3599 from Infisical/misc/updated-custom-cert-to-be-crt-formawt
misc: update custom cert to be crt format for docs
2025-05-14 14:24:29 +08:00
Sheen Capadngan
7c52e000cd misc: update custom cert to be crt format for docs 2025-05-14 14:12:08 +08:00
x032205
cccd4ba9e5 doc changes and other tweaks 2025-05-14 01:32:09 -04:00
x032205
63f0f8e299 final release 2025-05-14 01:16:42 -04:00
Maidul Islam
c8a3837432 refine docs 2025-05-13 22:02:49 -07:00
Vlad Matsiiako
2dd407b136 Merge pull request #3596 from Infisical/pulumi-documentation-update
Adding Pulumi documentation
2025-05-13 22:21:33 -06:00
Maidul Islam
4e1a5565d8 add linux upgrade docs 2025-05-13 20:40:29 -07:00
x032205
bae62421ae with stripSchema and filterForSchema 2025-05-13 23:08:54 -04:00
ArshBallagan
d397002704 Update pulumi.mdx 2025-05-13 20:29:06 -06:00
ArshBallagan
f5b1f671e3 Update pulumi.mdx 2025-05-13 20:17:23 -06:00
ArshBallagan
0597c5f0c0 Adding Pulumi documentation 2025-05-13 20:14:08 -06:00
Scott Wilson
eb3afc8034 Merge pull request #3595 from Infisical/remove-legacy-native-integrations-notice
improvement(native-integrations): Remove legacy badge/banner from native integrations UI
2025-05-13 18:51:03 -07:00
Scott Wilson
b67457fe93 chore: remove unused imports 2025-05-13 18:46:53 -07:00
Scott Wilson
75abdbe938 remove legacy badge/banner from native integrations UI 2025-05-13 18:41:14 -07:00
x032205
9b6a315825 Merge pull request #3593 from Infisical/ENG-2742
Fixed project roles not being editable in some cases
2025-05-13 17:10:23 -04:00
x032205
13b2f65b7e lint fix 2025-05-13 16:51:05 -04:00
x032205
6cf1e046b0 Fixed project roles not being editable in some cases 2025-05-13 16:38:26 -04:00
Scott Wilson
f6e1441dc0 Merge pull request #3570 from Infisical/policy-templates
feature(project-roles): Project Role Templates
2025-05-13 12:47:40 -07:00
Scott Wilson
7ed96164e5 improvement: address feedback 2025-05-13 12:25:24 -07:00
Scott Wilson
9eeb72ac80 fix: correct import 2025-05-13 12:18:35 -07:00
Scott Wilson
f6e566a028 merge main 2025-05-13 12:10:49 -07:00
x032205
a34c74e958 Merge pull request #3580 from Infisical/feat/return-metadata-with-identity-create
Return metadata with identity post endpoints
2025-05-13 14:22:34 -04:00
x032205
eef7a875a1 Merge pull request #3585 from Infisical/ENG-2748
feat(docs): Self approval
2025-05-13 14:05:59 -04:00
x032205
09938a911b nit fix 2025-05-13 13:58:52 -04:00
x032205
af08c41008 Merge pull request #3567 from Infisical/ENG-2636
feat(secret-sync): OCI Vault
2025-05-13 13:25:11 -04:00
x032205
443c8854ea Merge branch 'main' into ENG-2636 2025-05-13 13:16:59 -04:00
x032205
f7a25e7601 Merge pull request #3592 from Infisical/lint-fix
lint fix
2025-05-13 13:16:06 -04:00
x032205
4c6e5c9c4c lint fix 2025-05-13 13:11:20 -04:00
Maidul Islam
98a4e6c96d Merge pull request #3591 from akhilmhdh/fix/ui-skew
feat: added new cache control for index html
2025-05-13 12:30:50 -04:00
Maidul Islam
c93ce06409 Merge pull request #3589 from Infisical/misc/updated-org-delete-flow
misc: updated org delete flow to clear session
2025-05-13 11:41:09 -04:00
=
672e4baec4 feat: added new cache control for index html 2025-05-13 21:03:15 +05:30
Daniel Hougaard
8adf4787b9 Update 20250513081738_remove-gateway-project-link.ts 2025-05-13 15:31:13 +04:00
Daniel Hougaard
a12522db55 requested changes 2025-05-13 15:18:23 +04:00
Daniel Hougaard
49ab487dc2 Update organization-permissions.mdx 2025-05-13 15:04:21 +04:00
Daniel Hougaard
daf0731580 feat(gateways): decouple gateways from projects 2025-05-13 14:59:58 +04:00
Sheen
b5ef2a6837 Merge pull request #3569 from Infisical/pki-subscriber
Infisical PKI: Subscriber Functionality
2025-05-13 16:34:05 +08:00
x032205
52858dad79 Update docs/documentation/platform/pr-workflows.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-12 21:07:57 -04:00
x032205
1d7a6ea50e Update docs/documentation/platform/pr-workflows.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-12 21:07:34 -04:00
x032205
c031233247 feat(docs): Self approval 2025-05-12 21:04:05 -04:00
x032205
70fff1f2da review fixes 2025-05-12 19:38:00 -04:00
x032205
3f8eaa0679 remove schema change 2025-05-12 18:13:14 -04:00
Scott Wilson
50d0035d7b fix: correct remove oci secret if secret value is empty logic 2025-05-12 14:56:13 -07:00
Tuan Dang
9743ad02d5 Fix lint issues 2025-05-12 14:56:00 -07:00
x032205
50f5248e3e Merge branch 'main' into feat/return-metadata-with-identity-create 2025-05-12 17:50:13 -04:00
x032205
8d7b573988 final reviews 2025-05-12 17:39:29 -04:00
Tuan Dang
26d0ab1dc2 Fix lint issues 2025-05-12 14:34:14 -07:00
x032205
4acdbd24e9 remove useless schema 2025-05-12 16:50:47 -04:00
x032205
c3c907788a review fixes 2025-05-12 16:42:48 -04:00
Tuan Dang
bf833a57cd Fix merge conflicts 2025-05-12 12:59:54 -07:00
Tuan Dang
e8519f6612 Revise PR based on review 2025-05-12 12:56:56 -07:00
x032205
0b4675e7b5 Merge branch 'main' into ENG-2636 2025-05-12 14:56:01 -04:00
x032205
091e521180 review fixes 2025-05-12 14:49:45 -04:00
x032205
d5dbc7d7e0 erge branch 'daniel/unblock-dev' into ENG-2706 2025-05-12 10:52:40 -04:00
x032205
0af9415aa6 Merge branch 'main' into ENG-2706 2025-05-12 10:18:33 -04:00
Daniel Hougaard
fb2b64cb19 feat(identities/k8s): gateway support 2025-05-12 15:19:42 +04:00
x032205
2793ac22aa remove duplicate field 2025-05-11 22:27:09 -04:00
x032205
31fad03af8 Return metadata with identity post endpoints 2025-05-09 23:41:11 -04:00
x032205
ce612877b8 docs 2025-05-09 22:47:20 -04:00
x032205
4ad8b468d5 Merge branch 'main' into ENG-2706 2025-05-09 22:37:22 -04:00
x032205
5742fc648b add tenancy OCID requirement 2025-05-09 22:33:02 -04:00
Scott Wilson
aa68a3ef58 feature: add org role duplication 2025-05-09 14:29:18 -07:00
x032205
578a0d7d93 review fixes 2025-05-09 02:54:49 -04:00
x032205
cd71db416d cancel deletion + update on creation for scheduled for deletion secrets 2025-05-09 02:34:50 -04:00
x032205
9d682ca874 added RE2 to regex 2025-05-09 02:10:53 -04:00
x032205
9054db80ad truncation and UI tweaks 2025-05-09 02:05:30 -04:00
x032205
5bb8756c67 only list compartments which the user is authorized to 'use vaults' in 2025-05-09 01:49:34 -04:00
x032205
8b7cb4c4eb Merge branch 'main' into ENG-2636 2025-05-09 01:34:19 -04:00
x032205
a6ee6fc4ea docs, grammar fixes, frontend tweak 2025-05-09 01:29:11 -04:00
x032205
b21c17572d block local and private IPs on host header 2025-05-09 00:08:02 -04:00
Scott Wilson
44c7be54cf improvement: address feedback 2025-05-08 20:22:42 -07:00
Scott Wilson
45c08b3f09 improvement: improve role not found error display 2025-05-08 20:15:47 -07:00
Scott Wilson
57a29577fe feature: duplicate project role 2025-05-08 20:10:25 -07:00
x032205
2700a96df4 Remove unused package 2025-05-08 21:30:40 -04:00
x032205
7457ef3b66 bug fix 2025-05-08 21:24:03 -04:00
x032205
806df70dd7 tweaks 2025-05-08 21:03:58 -04:00
x032205
8eda358c17 schema gen 2025-05-08 20:59:05 -04:00
x032205
b34aabe72b merges 2025-05-08 20:56:04 -04:00
x032205
dfaed3c513 oci machine identity auth option 2025-05-08 20:42:58 -04:00
Scott Wilson
5b7627585f improvements: address feedback 2025-05-08 16:17:25 -07:00
Scott Wilson
800ea5ce78 feature: project role templates 2025-05-08 16:02:41 -07:00
Tuan Dang
531607dcb7 Revise pr based on greptile review 2025-05-08 10:37:33 -07:00
Tuan Dang
182de009b2 Fix lint issues 2025-05-08 10:01:44 -07:00
Tuan Dang
f1651ce171 Rename migration file 2025-05-08 09:10:49 -07:00
Tuan Dang
e1f563dbd4 Fix merge conflicts 2025-05-08 09:07:28 -07:00
Tuan Dang
107cca0b62 Complete preliminary docs for pki subscribers 2025-05-08 08:52:10 -07:00
x032205
72abc08f04 Merge branch 'main' into ENG-2636 2025-05-08 10:29:52 -04:00
x032205
d6b31cde44 greptile review fixes 2025-05-08 01:16:42 -04:00
x032205
2c94f9ec3c revert eslint memory increase 2025-05-08 00:50:31 -04:00
x032205
42ad63b58d increase max old space size for lint:fix 2025-05-08 00:44:03 -04:00
x032205
f2d5112585 Merge branch 'main' into ENG-2636 2025-05-08 00:27:28 -04:00
x032205
9c7b25de49 docs + tweaks 2025-05-08 00:25:19 -04:00
x032205
36954a9df9 secret sync + tweaks 2025-05-07 17:57:00 -04:00
x032205
581840a701 fixed app connection endpoints 2025-05-07 13:53:05 -04:00
x032205
326742c2d5 feat(app-connections): OCI 2025-05-07 10:59:27 -04:00
Daniel Hougaard
c891b8f5d3 fix routing 2025-05-07 03:00:20 +04:00
Tuan Dang
a32bb95703 Start work on PkiSubscriberDetailsByIDPage 2025-05-06 15:46:54 -07:00
Tuan Dang
0410c83cef Fix merge conflicts 2025-05-06 09:46:31 -07:00
Tuan Dang
cf4f2ea6b1 Begin developing pki subscriber 2025-05-06 09:44:57 -07:00
399 changed files with 14604 additions and 1452 deletions

View File

@@ -28,3 +28,15 @@ frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow
docs/cli/commands/user.mdx:generic-api-key:51 docs/cli/commands/user.mdx:generic-api-key:51
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76 frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188 docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188
cli/detect/config/gitleaks.toml:gcp-api-key:567
cli/detect/config/gitleaks.toml:gcp-api-key:569
cli/detect/config/gitleaks.toml:gcp-api-key:570
cli/detect/config/gitleaks.toml:gcp-api-key:572
cli/detect/config/gitleaks.toml:gcp-api-key:574
cli/detect/config/gitleaks.toml:gcp-api-key:575
cli/detect/config/gitleaks.toml:gcp-api-key:576
cli/detect/config/gitleaks.toml:gcp-api-key:577
cli/detect/config/gitleaks.toml:gcp-api-key:578
cli/detect/config/gitleaks.toml:gcp-api-key:579
cli/detect/config/gitleaks.toml:gcp-api-key:581
cli/detect/config/gitleaks.toml:gcp-api-key:582

1941
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,8 +38,8 @@
"build:frontend": "npm run build --prefix ../frontend", "build:frontend": "npm run build --prefix ../frontend",
"start": "node --enable-source-maps dist/main.mjs", "start": "node --enable-source-maps dist/main.mjs",
"type:check": "tsc --noEmit", "type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src", "lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'", "lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
"test:unit": "vitest run -c vitest.unit.config.ts", "test:unit": "vitest run -c vitest.unit.config.ts",
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1", "test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
@@ -209,6 +209,7 @@
"mysql2": "^3.9.8", "mysql2": "^3.9.8",
"nanoid": "^3.3.8", "nanoid": "^3.3.8",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"oci-sdk": "^2.108.0",
"odbc": "^2.4.9", "odbc": "^2.4.9",
"openid-client": "^5.6.5", "openid-client": "^5.6.5",
"ora": "^7.0.1", "ora": "^7.0.1",

View File

@@ -68,6 +68,7 @@ import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
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 { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
@@ -80,6 +81,7 @@ import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
@@ -208,6 +210,7 @@ declare module "fastify" {
identityGcpAuth: TIdentityGcpAuthServiceFactory; identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOciAuth: TIdentityOciAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory;
identityJwtAuth: TIdentityJwtAuthServiceFactory; identityJwtAuth: TIdentityJwtAuthServiceFactory;
identityLdapAuth: TIdentityLdapAuthServiceFactory; identityLdapAuth: TIdentityLdapAuthServiceFactory;
@@ -232,6 +235,7 @@ declare module "fastify" {
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory; certificateEst: TCertificateEstServiceFactory;
pkiCollection: TPkiCollectionServiceFactory; pkiCollection: TPkiCollectionServiceFactory;
pkiSubscriber: TPkiSubscriberServiceFactory;
secretScanning: TSecretScanningServiceFactory; secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory; license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory; trustedIp: TTrustedIpServiceFactory;

View File

@@ -119,6 +119,9 @@ import {
TIdentityMetadata, TIdentityMetadata,
TIdentityMetadataInsert, TIdentityMetadataInsert,
TIdentityMetadataUpdate, TIdentityMetadataUpdate,
TIdentityOciAuths,
TIdentityOciAuthsInsert,
TIdentityOciAuthsUpdate,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOidcAuthsInsert, TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate, TIdentityOidcAuthsUpdate,
@@ -209,6 +212,9 @@ import {
TPkiCollections, TPkiCollections,
TPkiCollectionsInsert, TPkiCollectionsInsert,
TPkiCollectionsUpdate, TPkiCollectionsUpdate,
TPkiSubscribers,
TPkiSubscribersInsert,
TPkiSubscribersUpdate,
TProjectBots, TProjectBots,
TProjectBotsInsert, TProjectBotsInsert,
TProjectBotsUpdate, TProjectBotsUpdate,
@@ -564,6 +570,11 @@ declare module "knex/types/tables" {
TPkiCollectionItemsInsert, TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate TPkiCollectionItemsUpdate
>; >;
[TableName.PkiSubscriber]: KnexOriginal.CompositeTableType<
TPkiSubscribers,
TPkiSubscribersInsert,
TPkiSubscribersUpdate
>;
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType< [TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
TUserGroupMembership, TUserGroupMembership,
TUserGroupMembershipInsert, TUserGroupMembershipInsert,
@@ -730,6 +741,11 @@ declare module "knex/types/tables" {
TIdentityAzureAuthsInsert, TIdentityAzureAuthsInsert,
TIdentityAzureAuthsUpdate TIdentityAzureAuthsUpdate
>; >;
[TableName.IdentityOciAuth]: KnexOriginal.CompositeTableType<
TIdentityOciAuths,
TIdentityOciAuthsInsert,
TIdentityOciAuthsUpdate
>;
[TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType< [TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType<
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOidcAuthsInsert, TIdentityOidcAuthsInsert,

View File

@@ -0,0 +1,46 @@
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.PkiSubscriber))) {
await knex.schema.createTable(TableName.PkiSubscriber, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("caId").nullable();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL");
t.string("name").notNullable();
t.string("commonName").notNullable();
t.specificType("subjectAlternativeNames", "text[]").notNullable();
t.string("ttl").notNullable();
t.specificType("keyUsages", "text[]").notNullable();
t.specificType("extendedKeyUsages", "text[]").notNullable();
t.string("status").notNullable(); // active / disabled
t.unique(["projectId", "name"]);
});
await createOnUpdateTrigger(knex, TableName.PkiSubscriber);
}
const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId");
if (!hasSubscriberCol) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("pkiSubscriberId").nullable();
t.foreign("pkiSubscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId");
if (hasSubscriberCol) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("pkiSubscriberId");
});
}
await knex.schema.dropTableIfExists(TableName.PkiSubscriber);
await dropOnUpdateTrigger(knex, TableName.PkiSubscriber);
}

View File

@@ -0,0 +1,30 @@
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.IdentityOciAuth))) {
await knex.schema.createTable(TableName.IdentityOciAuth, (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("type").notNullable();
t.string("tenancyOcid").notNullable();
t.string("allowedUsernames").nullable();
});
}
await createOnUpdateTrigger(knex, TableName.IdentityOciAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityOciAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityOciAuth);
}

View File

@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (!hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}

View File

@@ -0,0 +1,110 @@
import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TableName } from "../schemas";
import { getMigrationEnvConfig } from "./utils/env-config";
import { getMigrationEncryptionServices } from "./utils/services";
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
const BATCH_SIZE = 500;
export async function up(knex: Knex): Promise<void> {
// eslint-disable-next-line no-param-reassign
knex.replicaNode = () => {
return knex;
};
if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"))) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
table.index("gatewayId");
});
const existingDynamicSecretsWithProjectGatewayId = await knex(TableName.DynamicSecret)
.select(selectAllTableCols(TableName.DynamicSecret))
.whereNotNull(`${TableName.DynamicSecret}.projectGatewayId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.id`, `${TableName.DynamicSecret}.projectGatewayId`)
.whereNotNull(`${TableName.ProjectGateway}.gatewayId`)
.select(
knex.ref("projectId").withSchema(TableName.ProjectGateway).as("projectId"),
knex.ref("gatewayId").withSchema(TableName.ProjectGateway).as("projectGatewayGatewayId")
);
initLogger();
const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore();
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
const updatedDynamicSecrets = await Promise.all(
existingDynamicSecretsWithProjectGatewayId.map(async (existingDynamicSecret) => {
if (!existingDynamicSecret.projectGatewayGatewayId) {
const result = {
...existingDynamicSecret,
gatewayId: null
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
let decryptedStoredInput = JSON.parse(
secretManagerDecryptor({ cipherTextBlob: Buffer.from(existingDynamicSecret.encryptedInput) }).toString()
) as object;
// We're not removing the existing projectGatewayId from the input so we can easily rollback without having to re-encrypt the input
decryptedStoredInput = {
...decryptedStoredInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const encryptedInput = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(decryptedStoredInput))
}).cipherTextBlob;
const result = {
...existingDynamicSecret,
encryptedInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
})
);
for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.DynamicSecret)
.insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE))
.onConflict("id")
.merge();
}
}
}
export async function down(knex: Knex): Promise<void> {
// no re-encryption needed as we keep the old projectGatewayId in the input
if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId")) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}

View File

@@ -0,0 +1,53 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const columns = await knex.table(TableName.Organization).columnInfo();
await knex.schema.alterTable(TableName.Organization, (t) => {
if (!columns.secretsProductEnabled) {
t.boolean("secretsProductEnabled").defaultTo(true);
}
if (!columns.pkiProductEnabled) {
t.boolean("pkiProductEnabled").defaultTo(true);
}
if (!columns.kmsProductEnabled) {
t.boolean("kmsProductEnabled").defaultTo(true);
}
if (!columns.sshProductEnabled) {
t.boolean("sshProductEnabled").defaultTo(true);
}
if (!columns.scannerProductEnabled) {
t.boolean("scannerProductEnabled").defaultTo(true);
}
if (!columns.shareSecretsProductEnabled) {
t.boolean("shareSecretsProductEnabled").defaultTo(true);
}
});
}
export async function down(knex: Knex): Promise<void> {
const columns = await knex.table(TableName.Organization).columnInfo();
await knex.schema.alterTable(TableName.Organization, (t) => {
if (columns.secretsProductEnabled) {
t.dropColumn("secretsProductEnabled");
}
if (columns.pkiProductEnabled) {
t.dropColumn("pkiProductEnabled");
}
if (columns.kmsProductEnabled) {
t.dropColumn("kmsProductEnabled");
}
if (columns.sshProductEnabled) {
t.dropColumn("sshProductEnabled");
}
if (columns.scannerProductEnabled) {
t.dropColumn("scannerProductEnabled");
}
if (columns.shareSecretsProductEnabled) {
t.dropColumn("shareSecretsProductEnabled");
}
});
}

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSecretSharingColumn = await knex.schema.hasColumn(TableName.Project, "secretSharing");
if (!hasSecretSharingColumn) {
await knex.schema.table(TableName.Project, (table) => {
table.boolean("secretSharing").notNullable().defaultTo(true);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasSecretSharingColumn = await knex.schema.hasColumn(TableName.Project, "secretSharing");
if (hasSecretSharingColumn) {
await knex.schema.table(TableName.Project, (table) => {
table.dropColumn("secretSharing");
});
}
}

View File

@@ -0,0 +1,35 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasLifetimeColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretLifetime");
const hasViewLimitColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretViewLimit");
if (!hasLifetimeColumn || !hasViewLimitColumn) {
await knex.schema.alterTable(TableName.Organization, (t) => {
if (!hasLifetimeColumn) {
t.integer("maxSharedSecretLifetime").nullable().defaultTo(2592000); // 30 days in seconds
}
if (!hasViewLimitColumn) {
t.integer("maxSharedSecretViewLimit").nullable();
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasLifetimeColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretLifetime");
const hasViewLimitColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretViewLimit");
if (hasLifetimeColumn || hasViewLimitColumn) {
await knex.schema.alterTable(TableName.Organization, (t) => {
if (hasLifetimeColumn) {
t.dropColumn("maxSharedSecretLifetime");
}
if (hasViewLimitColumn) {
t.dropColumn("maxSharedSecretViewLimit");
}
});
}
}

View File

@@ -0,0 +1,43 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
const hasAuthorizedEmails = await knex.schema.hasColumn(TableName.SecretSharing, "authorizedEmails");
if (!hasEncryptedSalt || !hasAuthorizedEmails) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
// These two columns are only needed when secrets are shared with a specific list of emails
if (!hasEncryptedSalt) {
t.binary("encryptedSalt").nullable();
}
if (!hasAuthorizedEmails) {
t.json("authorizedEmails").nullable();
}
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
const hasAuthorizedEmails = await knex.schema.hasColumn(TableName.SecretSharing, "authorizedEmails");
if (hasEncryptedSalt || hasAuthorizedEmails) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
if (hasEncryptedSalt) {
t.dropColumn("encryptedSalt");
}
if (hasAuthorizedEmails) {
t.dropColumn("authorizedEmails");
}
});
}
}
}

View File

@@ -24,7 +24,8 @@ export const CertificatesSchema = z.object({
caCertId: z.string().uuid(), caCertId: z.string().uuid(),
certificateTemplateId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid().nullable().optional(),
keyUsages: z.string().array().nullable().optional(), keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional() extendedKeyUsages: z.string().array().nullable().optional(),
pkiSubscriberId: z.string().uuid().nullable().optional()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;

View File

@@ -27,7 +27,8 @@ export const DynamicSecretsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
encryptedInput: zodBuffer, encryptedInput: zodBuffer,
projectGatewayId: z.string().uuid().nullable().optional() projectGatewayId: z.string().uuid().nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -29,7 +29,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
allowedNames: z.string(), allowedNames: z.string(),
allowedAudience: z.string(), allowedAudience: z.string(),
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(), encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
}); });
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>; export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;

View File

@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityOciAuthsSchema = 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(),
type: z.string(),
tenancyOcid: z.string(),
allowedUsernames: z.string().nullable().optional()
});
export type TIdentityOciAuths = z.infer<typeof IdentityOciAuthsSchema>;
export type TIdentityOciAuthsInsert = Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>;
export type TIdentityOciAuthsUpdate = Partial<Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>>;

View File

@@ -37,6 +37,7 @@ export * from "./identity-gcp-auths";
export * from "./identity-jwt-auths"; export * from "./identity-jwt-auths";
export * from "./identity-kubernetes-auths"; export * from "./identity-kubernetes-auths";
export * from "./identity-metadata"; export * from "./identity-metadata";
export * from "./identity-oci-auths";
export * from "./identity-oidc-auths"; export * from "./identity-oidc-auths";
export * from "./identity-org-memberships"; export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege"; export * from "./identity-project-additional-privilege";
@@ -69,6 +70,7 @@ export * from "./organizations";
export * from "./pki-alerts"; export * from "./pki-alerts";
export * from "./pki-collection-items"; export * from "./pki-collection-items";
export * from "./pki-collections"; export * from "./pki-collections";
export * from "./pki-subscribers";
export * from "./project-bots"; export * from "./project-bots";
export * from "./project-environments"; export * from "./project-environments";
export * from "./project-gateways"; export * from "./project-gateways";

View File

@@ -21,6 +21,7 @@ export enum TableName {
CertificateBody = "certificate_bodies", CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets", CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates", CertificateTemplate = "certificate_templates",
PkiSubscriber = "pki_subscribers",
PkiAlert = "pki_alerts", PkiAlert = "pki_alerts",
PkiCollection = "pki_collections", PkiCollection = "pki_collections",
PkiCollectionItem = "pki_collection_items", PkiCollectionItem = "pki_collection_items",
@@ -78,6 +79,7 @@ export enum TableName {
IdentityAzureAuth = "identity_azure_auths", IdentityAzureAuth = "identity_azure_auths",
IdentityUaClientSecret = "identity_ua_client_secrets", IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths", IdentityAwsAuth = "identity_aws_auths",
IdentityOciAuth = "identity_oci_auths",
IdentityOidcAuth = "identity_oidc_auths", IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths", IdentityJwtAuth = "identity_jwt_auths",
IdentityLdapAuth = "identity_ldap_auths", IdentityLdapAuth = "identity_ldap_auths",
@@ -232,6 +234,7 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth", GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth", AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth", AZURE_AUTH = "azure-auth",
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth", OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth", JWT_AUTH = "jwt-auth",
LDAP_AUTH = "ldap-auth" LDAP_AUTH = "ldap-auth"

View File

@@ -28,7 +28,15 @@ export const OrganizationsSchema = z.object({
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(), privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
bypassOrgAuthEnabled: z.boolean().default(false), bypassOrgAuthEnabled: z.boolean().default(false),
userTokenExpiration: z.string().nullable().optional() userTokenExpiration: z.string().nullable().optional(),
secretsProductEnabled: z.boolean().default(true).nullable().optional(),
pkiProductEnabled: z.boolean().default(true).nullable().optional(),
kmsProductEnabled: z.boolean().default(true).nullable().optional(),
sshProductEnabled: z.boolean().default(true).nullable().optional(),
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
maxSharedSecretViewLimit: z.number().nullable().optional()
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;

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 { TImmutableDBKeys } from "./models";
export const PkiSubscribersSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string(),
caId: z.string().uuid().nullable().optional(),
name: z.string(),
commonName: z.string(),
subjectAlternativeNames: z.string().array(),
ttl: z.string(),
keyUsages: z.string().array(),
extendedKeyUsages: z.string().array(),
status: z.string()
});
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
export type TPkiSubscribersInsert = Omit<z.input<typeof PkiSubscribersSchema>, TImmutableDBKeys>;
export type TPkiSubscribersUpdate = Partial<Omit<z.input<typeof PkiSubscribersSchema>, TImmutableDBKeys>>;

View File

@@ -27,7 +27,8 @@ export const ProjectsSchema = z.object({
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
type: z.string(), type: z.string(),
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)
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -27,7 +27,9 @@ export const SecretSharingSchema = z.object({
password: z.string().nullable().optional(), password: z.string().nullable().optional(),
encryptedSecret: zodBuffer.nullable().optional(), encryptedSecret: zodBuffer.nullable().optional(),
identifier: z.string().nullable().optional(), identifier: z.string().nullable().optional(),
type: z.string().default("share") type: z.string().default("share"),
encryptedSalt: zodBuffer.nullable().optional(),
authorizedEmails: z.unknown().nullable().optional()
}); });
export type TSecretSharing = z.infer<typeof SecretSharingSchema>; export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

@@ -121,14 +121,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}), })
projects: z
.object({
name: z.string(),
id: z.string(),
slug: z.string()
})
.array()
}).array() }).array()
}) })
} }
@@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}), })
projectGatewayId: z.string()
}).array() }).array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const gateways = await server.services.gateway.getProjectGateways({ const gateways = await server.services.gateway.listGateways({
projectId: req.params.projectId, orgPermission: req.permission
projectPermission: req.permission
}); });
return { gateways }; return { gateways };
} }
@@ -216,8 +207,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
id: z.string() id: z.string()
}), }),
body: z.object({ body: z.object({
name: slugSchema({ field: "name" }).optional(), name: slugSchema({ field: "name" }).optional()
projectIds: z.string().array().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -230,8 +220,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gateway = await server.services.gateway.updateGatewayById({ const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission, orgPermission: req.permission,
id: req.params.id, id: req.params.id,
name: req.body.name, name: req.body.name
projectIds: req.body.projectIds
}); });
return { gateway }; return { gateway };
} }

View File

@@ -97,7 +97,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds) allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
}) })
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), { .refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
message: "Max TLL must be greater than or equal to TTL", message: "Max TTL must be greater than or equal to TTL",
path: ["maxTTL"] path: ["maxTTL"]
}), }),
response: { response: {

View File

@@ -73,7 +73,7 @@ export const registerSshHostRouter = 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 host = await server.services.sshHost.getSshHost({ const host = await server.services.sshHost.getSshHostById({
sshHostId: req.params.sshHostId, sshHostId: req.params.sshHostId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,

View File

@@ -19,7 +19,7 @@ import { TProjectPermission } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
@@ -162,6 +162,12 @@ export enum EventType {
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth", REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth", GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
LOGIN_IDENTITY_OCI_AUTH = "login-identity-oci-auth",
ADD_IDENTITY_OCI_AUTH = "add-identity-oci-auth",
UPDATE_IDENTITY_OCI_AUTH = "update-identity-oci-auth",
REVOKE_IDENTITY_OCI_AUTH = "revoke-identity-oci-auth",
GET_IDENTITY_OCI_AUTH = "get-identity-oci-auth",
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth", LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth", ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth", UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
@@ -254,6 +260,13 @@ export enum EventType {
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items", GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item", ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item", DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
CREATE_KMS = "create-kms", CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms", UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms", DELETE_KMS = "delete-kms",
@@ -1002,6 +1015,55 @@ interface GetIdentityAwsAuthEvent {
}; };
} }
interface LoginIdentityOciAuthEvent {
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
identityOciAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityOciAuthEvent {
type: EventType.ADD_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityOciAuthEvent {
type: EventType.REVOKE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityOciAuthEvent {
type: EventType.UPDATE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid?: string;
allowedUsernames: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityOciAuthEvent {
type: EventType.GET_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityAzureAuthEvent { interface LoginIdentityAzureAuthEvent {
type: EventType.LOGIN_IDENTITY_AZURE_AUTH; type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
metadata: { metadata: {
@@ -1965,6 +2027,77 @@ interface DeletePkiCollectionItem {
}; };
} }
interface CreatePkiSubscriber {
type: EventType.CREATE_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
caId?: string;
name: string;
commonName: string;
ttl: string;
subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
};
}
interface UpdatePkiSubscriber {
type: EventType.UPDATE_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
caId?: string;
name?: string;
commonName?: string;
ttl?: string;
subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
};
}
interface DeletePkiSubscriber {
type: EventType.DELETE_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
name: string;
};
}
interface GetPkiSubscriber {
type: EventType.GET_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
name: string;
};
}
interface IssuePkiSubscriberCert {
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT;
metadata: {
subscriberId: string;
name: string;
serialNumber: string;
};
}
interface SignPkiSubscriberCert {
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
metadata: {
subscriberId: string;
name: string;
serialNumber: string;
};
}
interface ListPkiSubscriberCerts {
type: EventType.LIST_PKI_SUBSCRIBER_CERTS;
metadata: {
subscriberId: string;
name: string;
projectId: string;
};
}
interface CreateKmsEvent { interface CreateKmsEvent {
type: EventType.CREATE_KMS; type: EventType.CREATE_KMS;
metadata: { metadata: {
@@ -2836,6 +2969,11 @@ export type Event =
| UpdateIdentityAwsAuthEvent | UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent | GetIdentityAwsAuthEvent
| DeleteIdentityAwsAuthEvent | DeleteIdentityAwsAuthEvent
| LoginIdentityOciAuthEvent
| AddIdentityOciAuthEvent
| UpdateIdentityOciAuthEvent
| GetIdentityOciAuthEvent
| DeleteIdentityOciAuthEvent
| LoginIdentityAzureAuthEvent | LoginIdentityAzureAuthEvent
| AddIdentityAzureAuthEvent | AddIdentityAzureAuthEvent
| DeleteIdentityAzureAuthEvent | DeleteIdentityAzureAuthEvent
@@ -2928,6 +3066,13 @@ export type Event =
| GetPkiCollectionItems | GetPkiCollectionItems
| AddPkiCollectionItem | AddPkiCollectionItem
| DeletePkiCollectionItem | DeletePkiCollectionItem
| CreatePkiSubscriber
| UpdatePkiSubscriber
| DeletePkiSubscriber
| GetPkiSubscriber
| IssuePkiSubscriberCert
| SignPkiSubscriberCert
| ListPkiSubscriberCerts
| CreateKmsEvent | CreateKmsEvent
| UpdateKmsEvent | UpdateKmsEvent
| DeleteKmsEvent | DeleteKmsEvent

View File

@@ -17,7 +17,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal"; import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import {
DynamicSecretStatus, DynamicSecretStatus,
@@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">; gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
}; };
@@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
@@ -117,15 +118,31 @@ export const dynamicSecretServiceFactory = ({
const inputs = await selectedProvider.validateProviderInputs(provider.inputs); const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) { if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const projectGatewayId = inputs.projectGatewayId as string; const gatewayId = inputs.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!projectGateway)
if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
@@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
name, name,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );
@@ -255,20 +272,30 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if ( if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
updatedInput && const gatewayId = updatedInput.gatewayId as string;
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!projectGateway) if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
@@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
status: null, status: null,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );

View File

@@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp"; import { TotpProvider } from "./totp";
type TBuildDynamicSecretProviderDTO = { type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
export const buildDynamicSecretProviders = ({ export const buildDynamicSecretProviders = ({

View File

@@ -137,7 +137,7 @@ export const DynamicSecretSqlDBSchema = z.object({
revocationStatement: z.string().trim(), revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(), renewStatement: z.string().trim().optional(),
ca: z.string().optional(), ca: z.string().optional(),
projectGatewayId: z.string().nullable().optional() gatewayId: z.string().nullable().optional()
}); });
export const DynamicSecretCassandraSchema = z.object({ export const DynamicSecretCassandraSchema = z.object({

View File

@@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => {
}; };
type TSqlDatabaseProviderDTO = { type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
}); });
@@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>, providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void> gatewayCallback: (host: string, port: number) => Promise<void>
) => { ) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string); const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy( await withGatewayProxy(
async (port) => { async (port) => {
@@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();

View File

@@ -1,37 +1,34 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>; export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
export const gatewayDALFactory = (db: TDbClient) => { export const gatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Gateway); const orm = ormify(db, TableName.Gateway);
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => { const find = async (
filter: TFindFilter<TGateways> & { orgId?: string },
{ offset, limit, sort, tx }: TFindOpt<TGateways> = {}
) => {
try { try {
const query = (tx || db)(TableName.Gateway) const query = (tx || db)(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter)) .where(buildFindFilter(filter, TableName.Gateway, ["orgId"]))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`) .join(
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`) TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.Gateway}.identityId`
)
.select(selectAllTableCols(TableName.Gateway)) .select(selectAllTableCols(TableName.Gateway))
.select( .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId"))
db.ref("name").withSchema(TableName.Identity).as("identityName"), .select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug"), if (filter.orgId) {
db.ref("id").withSchema(TableName.Project).as("projectId") void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId);
); }
if (limit) void query.limit(limit); if (limit) void query.limit(limit);
if (offset) void query.offset(offset); if (offset) void query.offset(offset);
if (sort) { if (sort) {
@@ -39,48 +36,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
} }
const docs = await query; const docs = await query;
return sqlNestRelationships({
data: docs, return docs.map((el) => ({
key: "id", ...GatewaysSchema.parse(el),
parentMapper: (data) => ({ orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string
...GatewaysSchema.parse(data), identity: { id: el.identityId, name: el.identityName }
identity: { id: data.identityId, name: data.identityName } }));
}),
childrenMapper: [
{
key: "projectId",
label: "projects" as const,
mapper: ({ projectId, projectName, projectSlug }) => ({
id: projectId,
name: projectName,
slug: projectSlug
})
}
]
});
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` }); throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
} }
}; };
const findByProjectId = async (projectId: string, tx?: Knex) => { return { ...orm, find };
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
)
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
}
};
return { ...orm, find, findByProjectId };
}; };

View File

@@ -4,7 +4,6 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -27,17 +26,14 @@ import { TGatewayDALFactory } from "./gateway-dal";
import { import {
TExchangeAllocatedRelayAddressDTO, TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO, TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
THeartBeatDTO, THeartBeatDTO,
TListGatewaysDTO, TListGatewaysDTO,
TUpdateGatewayByIdDTO TUpdateGatewayByIdDTO
} from "./gateway-types"; } from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal"; import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
type TGatewayServiceFactoryDep = { type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory; gatewayDAL: TGatewayDALFactory;
projectGatewayDAL: TProjectGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">; orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
@@ -57,8 +53,7 @@ export const gatewayServiceFactory = ({
kmsService, kmsService,
permissionService, permissionService,
orgGatewayConfigDAL, orgGatewayConfigDAL,
keyStore, keyStore
projectGatewayDAL
}: TGatewayServiceFactoryDep) => { }: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
// if (!licenseService.onPremFeatures.gateway) { // if (!licenseService.onPremFeatures.gateway) {
@@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => { const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
orgPermission.type, orgPermission.type,
orgPermission.id, orgPermission.id,
@@ -543,15 +538,6 @@ export const gatewayServiceFactory = ({
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name }); const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
if (projectIds) {
await projectGatewayDAL.transaction(async (tx) => {
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
await projectGatewayDAL.insertMany(
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
tx
);
});
}
return gateway; return gateway;
}; };
@@ -576,27 +562,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => { const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => {
await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
actorOrgId: projectPermission.orgId,
actorAuthMethod: projectPermission.authMethod,
actionProjectType: ActionProjectType.Any
});
const gateways = await gatewayDAL.findByProjectId(projectId);
return gateways;
};
// this has no permission check and used for dynamic secrets directly
// assumes permission check is already done
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
const { gatewayId } = projectGateway;
const gateway = await gatewayDAL.findById(gatewayId); const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
@@ -645,8 +611,7 @@ export const gatewayServiceFactory = ({
getGatewayById, getGatewayById,
updateGatewayById, updateGatewayById,
deleteGatewayById, deleteGatewayById,
getProjectGateways, fnGetGatewayClientTlsByGatewayId,
fnGetGatewayClientTls,
heartbeat heartbeat
}; };
}; };

View File

@@ -20,7 +20,6 @@ export type TGetGatewayByIdDTO = {
export type TUpdateGatewayByIdDTO = { export type TUpdateGatewayByIdDTO = {
id: string; id: string;
name?: string; name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor; orgPermission: OrgServiceActor;
}; };

View File

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

View File

@@ -714,13 +714,15 @@ export const oidcConfigServiceFactory = ({
} }
} }
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({ oidcLogin({
email: claims.email, email: claims.email,
externalId: claims.sub, externalId: claims.sub,
firstName: claims.given_name ?? "", firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "", lastName: claims.family_name ?? "",
orgId: org.id, orgId: org.id,
groups: claims.groups as string[] | undefined, groups,
callbackPort, callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships manageGroupMemberships: oidcCfg.manageGroupMemberships
}) })

View File

@@ -9,6 +9,7 @@ import {
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
@@ -76,6 +77,18 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SshHosts ProjectPermissionSub.SshHosts
); );
can(
[
ProjectPermissionPkiSubscriberActions.Edit,
ProjectPermissionPkiSubscriberActions.Read,
ProjectPermissionPkiSubscriberActions.Create,
ProjectPermissionPkiSubscriberActions.Delete,
ProjectPermissionPkiSubscriberActions.IssueCert,
ProjectPermissionPkiSubscriberActions.ListCerts
],
ProjectPermissionSub.PkiSubscribers
);
can( can(
[ [
ProjectPermissionMemberActions.Create, ProjectPermissionMemberActions.Create,
@@ -113,7 +126,6 @@ const buildAdminPermissionRules = () => {
can( can(
[ [
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Create,
@@ -194,7 +206,6 @@ const buildMemberPermissionRules = () => {
can( can(
[ [
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit, ProjectPermissionSecretActions.Edit,
@@ -338,6 +349,7 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers);
can( can(
[ [
@@ -372,9 +384,10 @@ const buildMemberPermissionRules = () => {
const buildViewerPermissionRules = () => { const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); can(
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); [ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue],
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); ProjectPermissionSub.Secrets
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);

View File

@@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions {
CreateGateways = "create-gateways", CreateGateways = "create-gateways",
ListGateways = "list-gateways", ListGateways = "list-gateways",
EditGateways = "edit-gateways", EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways" DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
} }
export enum OrgPermissionIdentityActions { export enum OrgPermissionIdentityActions {
@@ -337,6 +338,7 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -378,6 +380,7 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
return rules; return rules;
}; };

View File

@@ -87,6 +87,15 @@ export enum ProjectPermissionSshHostActions {
IssueHostCert = "issue-host-cert" IssueHostCert = "issue-host-cert"
} }
export enum ProjectPermissionPkiSubscriberActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert",
ListCerts = "list-certs"
}
export enum ProjectPermissionSecretSyncActions { export enum ProjectPermissionSecretSyncActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -143,6 +152,7 @@ export enum ProjectPermissionSub {
SshCertificateTemplates = "ssh-certificate-templates", SshCertificateTemplates = "ssh-certificate-templates",
SshHosts = "ssh-hosts", SshHosts = "ssh-hosts",
SshHostGroups = "ssh-host-groups", SshHostGroups = "ssh-host-groups",
PkiSubscribers = "pki-subscribers",
PkiAlerts = "pki-alerts", PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections", PkiCollections = "pki-collections",
Kms = "kms", Kms = "kms",
@@ -190,6 +200,11 @@ export type SshHostSubjectFields = {
hostname: string; hostname: string;
}; };
export type PkiSubscriberSubjectFields = {
name: string;
// (dangtony98): consider adding [commonName] as a subject field in the future
};
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
@@ -249,6 +264,13 @@ export type ProjectPermissionSet =
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields) ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
] ]
| [
ProjectPermissionPkiSubscriberActions,
(
| ProjectPermissionSub.PkiSubscribers
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
@@ -399,6 +421,21 @@ const SshHostConditionSchema = z
}) })
.partial(); .partial();
const PkiSubscriberConditionSchema = z
.object({
name: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
@@ -663,6 +700,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.PkiSubscribers).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiSubscriberActions).describe(
"Describe what action an entity can take."
),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
conditions: PkiSubscriberConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."),

View File

@@ -186,13 +186,42 @@ export const sshHostGroupServiceFactory = ({
}); });
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => { const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
await sshHostGroupDAL.updateById( if (name && name !== sshHostGroup.name) {
sshHostGroupId, // (dangtony98): room to optimize check to ensure that
{ // the SSH host group name is unique across the whole org
name const project = await projectDAL.findById(sshHostGroup.projectId, tx);
}, if (!project) throw new NotFoundError({ message: `Project with ID '${sshHostGroup.projectId}' not found` });
tx const projects = await projectDAL.find(
); {
orgId: project.orgId
},
{ tx }
);
const existingSshHostGroup = await sshHostGroupDAL.find(
{
name,
$in: {
projectId: projects.map((p) => p.id)
}
},
{ tx }
);
if (existingSshHostGroup.length) {
throw new BadRequestError({
message: `SSH host group with name '${name}' already exists in the organization`
});
}
await sshHostGroupDAL.updateById(
sshHostGroupId,
{
name
},
tx
);
}
if (loginMappings) { if (loginMappings) {
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx); await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
if (loginMappings.length) { if (loginMappings.length) {

View File

@@ -335,7 +335,7 @@ export const sshHostServiceFactory = ({
return host; return host;
}; };
const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => { const getSshHostById = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
if (!host) { if (!host) {
throw new NotFoundError({ throw new NotFoundError({
@@ -631,7 +631,7 @@ export const sshHostServiceFactory = ({
createSshHost, createSshHost,
updateSshHost, updateSshHost,
deleteSshHost, deleteSshHost,
getSshHost, getSshHostById,
issueSshHostUserCert, issueSshHostUserCert,
issueSshHostHostCert, issueSshHostHostCert,
getSshHostUserCaPk, getSshHostUserCaPk,

View File

@@ -14,6 +14,7 @@ export enum ApiDocsTags {
UniversalAuth = "Universal Auth", UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth", GcpAuth = "GCP Auth",
AwsAuth = "AWS Auth", AwsAuth = "AWS Auth",
OciAuth = "OCI Auth",
AzureAuth = "Azure Auth", AzureAuth = "Azure Auth",
KubernetesAuth = "Kubernetes Auth", KubernetesAuth = "Kubernetes Auth",
JwtAuth = "JWT Auth", JwtAuth = "JWT Auth",
@@ -46,6 +47,7 @@ export enum ApiDocsTags {
PkiCertificateTemplates = "PKI Certificate Templates", PkiCertificateTemplates = "PKI Certificate Templates",
PkiCertificateCollections = "PKI Certificate Collections", PkiCertificateCollections = "PKI Certificate Collections",
PkiAlerting = "PKI Alerting", PkiAlerting = "PKI Alerting",
PkiSubscribers = "PKI Subscribers",
SshCertificates = "SSH Certificates", SshCertificates = "SSH Certificates",
SshCertificateAuthorities = "SSH Certificate Authorities", SshCertificateAuthorities = "SSH Certificate Authorities",
SshCertificateTemplates = "SSH Certificate Templates", SshCertificateTemplates = "SSH Certificate Templates",
@@ -270,6 +272,40 @@ export const AWS_AUTH = {
} }
} as const; } as const;
export const OCI_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
userOcid: "The OCID of the user attempting login.",
headers: "The headers of the signed request."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
}
} as const;
export const AZURE_AUTH = { export const AZURE_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the identity to login."
@@ -357,6 +393,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience: allowedAudience:
"The optional audience claim that the service account JWT token must have to authenticate with Infisical.", "The optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
@@ -373,6 +410,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience: allowedAudience:
"The new optional audience claim that the service account JWT token must have to authenticate with Infisical.", "The new optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.", accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
@@ -570,7 +608,8 @@ export const PROJECTS = {
projectDescription: "An optional description label for the project.", projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project.",
slug: "An optional slug for the project. (must be unique within the organization)", slug: "An optional slug for the project. (must be unique within the organization)",
hasDeleteProtection: "Enable or disable delete protection for the project." hasDeleteProtection: "Enable or disable delete protection for the project.",
secretSharing: "Enable or disable secret sharing for the project."
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
@@ -639,6 +678,9 @@ export const PROJECTS = {
commonName: "The common name of the certificate to filter by.", commonName: "The common name of the certificate to filter by.",
offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.", offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.",
limit: "The number of certificates to return." limit: "The number of certificates to return."
},
LIST_PKI_SUBSCRIBERS: {
projectId: "The ID of the project to list PKI subscribers for."
} }
} as const; } as const;
@@ -1731,6 +1773,67 @@ export const ALERTS = {
} }
}; };
export const PKI_SUBSCRIBERS = {
GET: {
subscriberName: "The name of the PKI subscriber to get.",
projectId: "The ID of the project to get the PKI subscriber for."
},
CREATE: {
projectId: "The ID of the project to create the PKI subscriber in.",
caId: "The ID of the CA that will issue certificates for the PKI subscriber.",
name: "The name of the PKI subscriber.",
commonName: "The common name (CN) to be used on certificates issued for this subscriber.",
status: "The status of the PKI subscriber. This can be one of active or disabled.",
ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...",
subjectAlternativeNames:
"A list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
keyUsages: "The key usage extension to be used on certificates issued for this subscriber.",
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber."
},
UPDATE: {
projectId: "The ID of the project to update the PKI subscriber in.",
subscriberName: "The name of the PKI subscriber to update.",
caId: "The ID of the CA that will issue certificates for the PKI subscriber to update to.",
name: "The name of the PKI subscriber to update to.",
commonName: "The common name (CN) to be used on certificates issued for this subscriber to update to.",
status: "The status of the PKI subscriber to update to. This can be one of active or disabled.",
ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...",
subjectAlternativeNames:
"A comma-delimited list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.",
extendedKeyUsages:
"The extended key usage extension to be used on certificates issued for this subscriber to update to."
},
DELETE: {
subscriberName: "The name of the PKI subscriber to delete.",
projectId: "The ID of the project of the PKI subscriber to delete."
},
ISSUE_CERT: {
subscriberName: "The name of the PKI subscriber to issue the certificate for.",
projectId: "The ID of the project of the PKI subscriber to issue the certificate for.",
certificate: "The issued certificate.",
issuingCaCertificate: "The certificate of the issuing CA.",
certificateChain: "The certificate chain of the issued certificate.",
privateKey: "The private key of the issued certificate.",
serialNumber: "The serial number of the issued certificate."
},
SIGN_CERT: {
subscriberName: "The name of the PKI subscriber to sign the certificate for.",
projectId: "The ID of the project of the PKI subscriber to sign the certificate for.",
csr: "The CSR to be used to sign the certificate.",
certificate: "The signed certificate.",
issuingCaCertificate: "The certificate of the issuing CA.",
certificateChain: "The certificate chain of the signed certificate.",
serialNumber: "The serial number of the signed certificate."
},
LIST_CERTS: {
subscriberName: "The name of the PKI subscriber to list the certificates for.",
projectId: "The ID of the project of the PKI subscriber to list the certificates for.",
offset: "The offset to start from.",
limit: "The number of certificates to return."
}
};
export const PKI_COLLECTIONS = { export const PKI_COLLECTIONS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the PKI collection in.", projectId: "The ID of the project to create the PKI collection in.",
@@ -1974,6 +2077,13 @@ export const AppConnections = {
AZURE_CLIENT_SECRETS: { AZURE_CLIENT_SECRETS: {
code: "The OAuth code to use to connect with Azure Client Secrets.", code: "The OAuth code to use to connect with Azure Client Secrets.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets." tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
},
OCI: {
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.",
fingerprint: "The fingerprint of the public key uploaded to the user's API keys.",
privateKey: "The private key content in PEM format used to sign API requests."
} }
} }
}; };
@@ -2037,6 +2147,7 @@ export const SecretSyncs = {
const destinationName = SECRET_SYNC_NAME_MAP[destination]; const destinationName = SECRET_SYNC_NAME_MAP[destination];
return { return {
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
keySchema: `Specify the format to use for structuring secret keys in the ${destinationName} destination.`,
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.` disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
}; };
}, },
@@ -2121,6 +2232,11 @@ export const SecretSyncs = {
TEAMCITY: { TEAMCITY: {
project: "The TeamCity project to sync secrets to.", project: "The TeamCity project to sync secrets to.",
buildConfig: "The TeamCity build configuration to sync secrets to." buildConfig: "The TeamCity build configuration to sync secrets to."
},
OCI_VAULT: {
compartmentOcid: "The OCID (Oracle Cloud Identifier) of the compartment where the vault is located.",
vaultOcid: "The OCID (Oracle Cloud Identifier) of the vault to sync secrets to.",
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault."
} }
} }
}; };

View File

@@ -174,6 +174,8 @@ const setupProxyServer = async ({
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = net.createServer(); const server = net.createServer();
let streamClosed = false;
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientConn) => { server.on("connection", async (clientConn) => {
try { try {
@@ -202,9 +204,15 @@ const setupProxyServer = async ({
// Handle client connection close // Handle client connection close
clientConn.on("end", () => { clientConn.on("end", () => {
writer.close().catch((err) => { if (!streamClosed) {
logger.error(err); try {
}); writer.close().catch((err) => {
logger.debug(err, "Error closing writer (already closed)");
});
} catch (error) {
logger.debug(error, "Error in writer close");
}
}
}); });
clientConn.on("error", (clientConnErr) => { clientConn.on("error", (clientConnErr) => {
@@ -249,14 +257,29 @@ const setupProxyServer = async ({
setupCopy(); setupCopy();
// Handle connection closure // Handle connection closure
clientConn.on("close", () => { clientConn.on("close", () => {
stream.destroy().catch((err) => { if (!streamClosed) {
proxyErrorMsg.push((err as Error)?.message); streamClosed = true;
}); stream.destroy().catch((err) => {
logger.debug(err, "Stream already destroyed during close event");
});
}
}); });
const cleanup = async () => { const cleanup = async () => {
clientConn?.destroy(); try {
await stream.destroy(); clientConn?.destroy();
} catch (err) {
logger.debug(err, "Error destroying client connection");
}
if (!streamClosed) {
streamClosed = true;
try {
await stream.destroy();
} catch (err) {
logger.debug(err, "Error destroying stream (might be already closed)");
}
}
}; };
clientConn.on("error", (clientConnErr) => { clientConn.on("error", (clientConnErr) => {
@@ -301,8 +324,17 @@ const setupProxyServer = async ({
server, server,
port: address.port, port: address.port,
cleanup: async () => { cleanup: async () => {
server.close(); try {
await quicClient?.destroy(); server.close();
} catch (err) {
logger.debug(err, "Error closing server");
}
try {
await quicClient?.destroy();
} catch (err) {
logger.debug(err, "Error destroying QUIC client");
}
}, },
getProxyError: () => proxyErrorMsg.join(",") getProxyError: () => proxyErrorMsg.join(",")
}); });
@@ -320,10 +352,10 @@ interface ProxyOptions {
orgId: string; orgId: string;
} }
export const withGatewayProxy = async ( export const withGatewayProxy = async <T>(
callback: (port: number) => Promise<void>, callback: (port: number) => Promise<T>,
options: ProxyOptions options: ProxyOptions
): Promise<void> => { ): Promise<T> => {
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options; const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
// Setup the proxy server // Setup the proxy server
@@ -339,7 +371,7 @@ export const withGatewayProxy = async (
try { try {
// Execute the callback with the allocated port // Execute the callback with the allocated port
await callback(port); return await callback(port);
} catch (err) { } catch (err) {
const proxyErrorMessage = getProxyError(); const proxyErrorMessage = getProxyError();
if (proxyErrorMessage) { if (proxyErrorMessage) {

View File

@@ -32,13 +32,13 @@ export const buildFindFilter =
<R extends object = object>( <R extends object = object>(
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>, { $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
tableName?: TableName, tableName?: TableName,
excludeKeys?: Array<keyof R> excludeKeys?: string[]
) => ) =>
(bd: Knex.QueryBuilder<R, R>) => { (bd: Knex.QueryBuilder<R, R>) => {
const processedFilter = tableName const processedFilter = tableName
? Object.fromEntries( ? Object.fromEntries(
Object.entries(filter) Object.entries(filter)
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R)) .filter(([key]) => !excludeKeys || !excludeKeys.includes(key))
.map(([key, value]) => [`${tableName}.${key}`, value]) .map(([key, value]) => [`${tableName}.${key}`, value])
) )
: filter; : filter;

View File

@@ -57,7 +57,9 @@ export const registerServeUI = async (
reply.callNotFound(); reply.callNotFound();
return; return;
} }
return reply.sendFile("index.html"); // reference: https://github.com/fastify/fastify-static?tab=readme-ov-file#managing-cache-control-headers
// to avoid ui bundle skew on new deployment
return reply.sendFile("index.html", { maxAge: 0, immutable: false });
} }
}); });
} }

View File

@@ -32,7 +32,6 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal";
@@ -162,6 +161,8 @@ import { identityKubernetesAuthDALFactory } from "@app/services/identity-kuberne
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal"; import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
import { identityOciAuthDALFactory } from "@app/services/identity-oci-auth/identity-oci-auth-dal";
import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
@@ -197,6 +198,8 @@ import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-servic
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal"; import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { projectDALFactory } from "@app/services/project/project-dal"; import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectQueueFactory } from "@app/services/project/project-queue";
import { projectServiceFactory } from "@app/services/project/project-service"; import { projectServiceFactory } from "@app/services/project/project-service";
@@ -353,6 +356,7 @@ export const registerRoutes = async (
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const identityOciAuthDAL = identityOciAuthDALFactory(db);
const identityOidcAuthDAL = identityOidcAuthDALFactory(db); const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityJwtAuthDAL = identityJwtAuthDALFactory(db); const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
@@ -434,7 +438,6 @@ export const registerRoutes = async (
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db); const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db); const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db); const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
const githubOrgSyncDAL = githubOrgSyncDALFactory(db); const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
@@ -828,6 +831,7 @@ export const registerRoutes = async (
const pkiAlertDAL = pkiAlertDALFactory(db); const pkiAlertDAL = pkiAlertDALFactory(db);
const pkiCollectionDAL = pkiCollectionDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db);
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db); const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
const pkiSubscriberDAL = pkiSubscriberDALFactory(db);
const certificateService = certificateServiceFactory({ const certificateService = certificateServiceFactory({
certificateDAL, certificateDAL,
@@ -962,6 +966,20 @@ export const registerRoutes = async (
projectDAL projectDAL
}); });
const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService,
permissionService
});
const projectTemplateService = projectTemplateServiceFactory({ const projectTemplateService = projectTemplateServiceFactory({
licenseService, licenseService,
permissionService, permissionService,
@@ -1059,6 +1077,7 @@ export const registerRoutes = async (
projectRoleDAL, projectRoleDAL,
folderDAL, folderDAL,
licenseService, licenseService,
pkiSubscriberDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
pkiAlertDAL, pkiAlertDAL,
@@ -1401,12 +1420,24 @@ export const registerRoutes = async (
identityUaDAL, identityUaDAL,
licenseService licenseService
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
licenseService, licenseService,
gatewayService,
gatewayDAL,
kmsService kmsService
}); });
const identityGcpAuthService = identityGcpAuthServiceFactory({ const identityGcpAuthService = identityGcpAuthServiceFactory({
@@ -1433,6 +1464,14 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const identityOciAuthService = identityOciAuthServiceFactory({
identityAccessTokenDAL,
identityOciAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
});
const identityOidcAuthService = identityOidcAuthServiceFactory({ const identityOidcAuthService = identityOidcAuthServiceFactory({
identityOidcAuthDAL, identityOidcAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
@@ -1461,16 +1500,6 @@ export const registerRoutes = async (
identityDAL identityDAL
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({ const dynamicSecretProviders = buildDynamicSecretProviders({
gatewayService gatewayService
}); });
@@ -1492,7 +1521,7 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService, licenseService,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}); });
@@ -1719,6 +1748,7 @@ export const registerRoutes = async (
identityGcpAuth: identityGcpAuthService, identityGcpAuth: identityGcpAuthService,
identityAwsAuth: identityAwsAuthService, identityAwsAuth: identityAwsAuthService,
identityAzureAuth: identityAzureAuthService, identityAzureAuth: identityAzureAuthService,
identityOciAuth: identityOciAuthService,
identityOidcAuth: identityOidcAuthService, identityOidcAuth: identityOidcAuthService,
identityJwtAuth: identityJwtAuthService, identityJwtAuth: identityJwtAuthService,
identityLdapAuth: identityLdapAuthService, identityLdapAuth: identityLdapAuthService,
@@ -1745,6 +1775,7 @@ export const registerRoutes = async (
certificateEst: certificateEstService, certificateEst: certificateEstService,
pkiAlert: pkiAlertService, pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService, pkiCollection: pkiCollectionService,
pkiSubscriber: pkiSubscriberService,
secretScanning: secretScanningService, secretScanning: secretScanningService,
license: licenseService, license: licenseService,
trustedIp: trustedIpService, trustedIp: trustedIpService,

View File

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

View File

@@ -38,6 +38,7 @@ import {
} from "@app/services/app-connection/humanitec"; } from "@app/services/app-connection/humanitec";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci";
import { import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema SanitizedPostgresConnectionSchema
@@ -76,7 +77,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureClientSecretsConnectionSchema.options, ...SanitizedAzureClientSecretsConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options,
...SanitizedLdapConnectionSchema.options, ...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options ...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -97,7 +99,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureClientSecretsConnectionListItemSchema, AzureClientSecretsConnectionListItemSchema,
WindmillConnectionListItemSchema, WindmillConnectionListItemSchema,
LdapConnectionListItemSchema, LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -13,6 +13,7 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerOCIConnectionRouter } from "./oci-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
@@ -40,5 +41,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Auth0]: registerAuth0ConnectionRouter, [AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.HCVault]: registerHCVaultConnectionRouter, [AppConnection.HCVault]: registerHCVaultConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter, [AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter [AppConnection.TeamCity]: registerTeamCityConnectionRouter,
[AppConnection.OCI]: registerOCIConnectionRouter
}; };

View File

@@ -0,0 +1,123 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateOCIConnectionSchema,
SanitizedOCIConnectionSchema,
UpdateOCIConnectionSchema
} from "@app/services/app-connection/oci";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.OCI,
server,
sanitizedResponseSchema: SanitizedOCIConnectionSchema,
createSchema: CreateOCIConnectionSchema,
updateSchema: UpdateOCIConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/compartments`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const compartments = await server.services.appConnection.oci.listCompartments(connectionId, req.permission);
return compartments;
}
});
server.route({
method: "GET",
url: `/:connectionId/vaults`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
compartmentOcid: z.string().min(1, "Compartment OCID required")
}),
response: {
200: z
.object({
id: z.string(),
displayName: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { compartmentOcid } = req.query;
const vaults = await server.services.appConnection.oci.listVaults(
{ connectionId, compartmentOcid },
req.permission
);
return vaults;
}
});
server.route({
method: "GET",
url: `/:connectionId/vault-keys`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
compartmentOcid: z.string().min(1, "Compartment OCID required"),
vaultOcid: z.string().min(1, "Vault OCID required")
}),
response: {
200: z
.object({
id: z.string(),
displayName: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { compartmentOcid, vaultOcid } = req.query;
const keys = await server.services.appConnection.oci.listVaultKeys(
{ connectionId, compartmentOcid, vaultOcid },
req.permission
);
return keys;
}
});
};

View File

@@ -131,8 +131,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain), certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey), privateKey: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
}) })
} }
@@ -518,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain), certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
}) })
} }

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas"; import { IdentityKubernetesAuthsSchema } 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 { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { 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";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -21,7 +22,8 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
kubernetesHost: true, kubernetesHost: true,
allowedNamespaces: true, allowedNamespaces: true,
allowedNames: true, allowedNames: true,
allowedAudience: true allowedAudience: true,
gatewayId: true
}).extend({ }).extend({
caCert: z.string(), caCert: z.string(),
tokenReviewerJwt: z.string().optional().nullable() tokenReviewerJwt: z.string().optional().nullable()
@@ -100,12 +102,32 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}), }),
body: z body: z
.object({ .object({
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), kubernetesHost: z
.string()
.trim()
.min(1)
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
.refine(
(val) =>
characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash,
CharacterType.Hyphen
])(val),
{
message:
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
}
),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.ATTACH.gatewayId),
accessTokenTrustedIps: z accessTokenTrustedIps: z
.object({ .object({
ipAddress: z.string().trim() ipAddress: z.string().trim()
@@ -199,12 +221,36 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}), }),
body: z body: z
.object({ .object({
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), kubernetesHost: z
.string()
.trim()
.min(1)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
.refine(
(val) => {
if (!val) return true;
return characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash,
CharacterType.Hyphen
])(val);
},
{
message:
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
}
),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.UPDATE.gatewayId),
accessTokenTrustedIps: z accessTokenTrustedIps: z
.object({ .object({
ipAddress: z.string().trim() ipAddress: z.string().trim()

View File

@@ -0,0 +1,338 @@
import { z } from "zod";
import { IdentityOciAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateTenancy, validateUsernames } from "@app/services/identity-oci-auth/identity-oci-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/oci-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Login with OCI Auth",
body: z.object({
identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId),
userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid),
headers: z
.object({
authorization: z.string(),
host: z.string(),
"x-date": z.string()
})
.describe(OCI_AUTH.LOGIN.headers)
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityOciAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityOciAuthId: identityOciAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Attach OCI Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(OCI_AUTH.ATTACH.identityId)
}),
body: z
.object({
tenancyOcid: validateTenancy.describe(OCI_AUTH.ATTACH.tenancyOcid),
allowedUsernames: validateUsernames.describe(OCI_AUTH.ATTACH.allowedUsernames),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(OCI_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(OCI_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(OCI_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OCI_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.attachOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
tenancyOcid: identityOciAuth.tenancyOcid,
allowedUsernames: identityOciAuth.allowedUsernames || null,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "PATCH",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Update OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.UPDATE.identityId)
}),
body: z
.object({
tenancyOcid: validateTenancy.describe(OCI_AUTH.UPDATE.tenancyOcid),
allowedUsernames: validateUsernames.describe(OCI_AUTH.UPDATE.allowedUsernames),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(OCI_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(OCI_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(OCI_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.min(0)
.optional()
.describe(OCI_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.updateOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
allowedUsernames: req.body.allowedUsernames || null
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
tenancyOcid: identityOciAuth.tenancyOcid,
allowedUsernames: identityOciAuth.allowedUsernames || null,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "GET",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Retrieve OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.RETRIEVE.identityId)
}),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.getOciAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.GET_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "DELETE",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Delete OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.revokeIdentityOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId
}
}
});
return { identityOciAuth };
}
});
};

View File

@@ -52,7 +52,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
identity: IdentitiesSchema.extend({ identity: IdentitiesSchema.extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string()),
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
}) })
}) })
} }
@@ -123,7 +124,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
identity: IdentitiesSchema identity: IdentitiesSchema.extend({
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
})
}) })
} }
}, },
@@ -227,8 +230,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
identity: IdentityOrgMembershipsSchema.extend({ identity: IdentityOrgMembershipsSchema.extend({
metadata: z metadata: z
.object({ .object({
key: z.string().trim().min(1),
id: z.string().trim().min(1), id: z.string().trim().min(1),
key: z.string().trim().min(1),
value: z.string().trim().min(1) value: z.string().trim().min(1)
}) })
.array() .array()

View File

@@ -20,6 +20,7 @@ import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router"; import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -33,6 +34,7 @@ import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router"; import { registerPasswordRouter } from "./password-router";
import { registerPkiAlertRouter } from "./pki-alert-router"; import { registerPkiAlertRouter } from "./pki-alert-router";
import { registerPkiCollectionRouter } from "./pki-collection-router"; import { registerPkiCollectionRouter } from "./pki-collection-router";
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectMembershipRouter } from "./project-membership-router";
@@ -62,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAwsAuthRouter);
await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOciAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter);
await authRouter.register(registerIdentityJwtAuthRouter); await authRouter.register(registerIdentityJwtAuthRouter);
await authRouter.register(registerIdentityLdapAuthRouter); await authRouter.register(registerIdentityLdapAuthRouter);
@@ -105,6 +108,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
}, },
{ prefix: "/pki" } { prefix: "/pki" }
); );

View File

@@ -275,6 +275,23 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}, },
{ message: "Duration value must be at least 1" } { message: "Duration value must be at least 1" }
) )
.optional(),
secretsProductEnabled: z.boolean().optional(),
pkiProductEnabled: z.boolean().optional(),
kmsProductEnabled: z.boolean().optional(),
sshProductEnabled: z.boolean().optional(),
scannerProductEnabled: z.boolean().optional(),
shareSecretsProductEnabled: z.boolean().optional(),
maxSharedSecretLifetime: z
.number()
.min(300, "Max Shared Secret lifetime cannot be under 5 minutes")
.max(2592000, "Max Shared Secret lifetime cannot exceed 30 days")
.optional(),
maxSharedSecretViewLimit: z
.number()
.min(1, "Max Shared Secret view count cannot be lower than 1")
.max(1000, "Max Shared Secret view count cannot exceed 1000")
.nullable()
.optional() .optional()
}), }),
response: { response: {

View File

@@ -0,0 +1,478 @@
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { validateAltNameField } from "@app/services/certificate-authority/certificate-authority-validators";
import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema";
import { PkiSubscriberStatus } from "@app/services/pki-subscriber/pki-subscriber-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:subscriberName",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Get PKI Subscriber",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName)
}),
querystring: z.object({
projectId: z.string().describe(PKI_SUBSCRIBERS.GET.projectId)
}),
response: {
200: sanitizedPkiSubscriber
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const subscriber = await server.services.pkiSubscriber.getSubscriber({
subscriberName: req.params.subscriberName,
projectId: req.query.projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: subscriber.projectId,
event: {
type: EventType.GET_PKI_SUBSCRIBER,
metadata: {
pkiSubscriberId: subscriber.id,
name: subscriber.name
}
}
});
return subscriber;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Create PKI Subscriber",
body: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.CREATE.projectId),
caId: z
.string()
.trim()
.uuid("CA ID must be a valid UUID")
.min(1, "CA ID is required")
.describe(PKI_SUBSCRIBERS.CREATE.caId),
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.CREATE.name),
commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.CREATE.commonName),
status: z
.nativeEnum(PkiSubscriberStatus)
.default(PkiSubscriberStatus.ACTIVE)
.describe(PKI_SUBSCRIBERS.CREATE.status),
ttl: z
.string()
.trim()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(PKI_SUBSCRIBERS.CREATE.ttl),
subjectAlternativeNames: validateAltNameField
.array()
.default([])
.transform((arr) => Array.from(new Set(arr)))
.describe(PKI_SUBSCRIBERS.CREATE.subjectAlternativeNames),
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT])
.transform((arr) => Array.from(new Set(arr)))
.describe(PKI_SUBSCRIBERS.CREATE.keyUsages),
extendedKeyUsages: z
.nativeEnum(CertExtendedKeyUsage)
.array()
.default([])
.transform((arr) => Array.from(new Set(arr)))
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages)
}),
response: {
200: sanitizedPkiSubscriber
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const subscriber = await server.services.pkiSubscriber.createSubscriber({
...req.body,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: subscriber.projectId,
event: {
type: EventType.CREATE_PKI_SUBSCRIBER,
metadata: {
pkiSubscriberId: subscriber.id,
caId: subscriber.caId ?? undefined,
name: subscriber.name,
commonName: subscriber.commonName,
ttl: subscriber.ttl,
subjectAlternativeNames: subscriber.subjectAlternativeNames,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
}
}
});
return subscriber;
}
});
server.route({
method: "PATCH",
url: "/:subscriberName",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Update PKI Subscriber",
params: z.object({
subscriberName: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.subscriberName)
}),
body: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.projectId),
caId: z
.string()
.trim()
.uuid("CA ID must be a valid UUID")
.min(1, "CA ID is required")
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.caId),
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.UPDATE.name).optional(),
commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.UPDATE.commonName).optional(),
status: z.nativeEnum(PkiSubscriberStatus).optional().describe(PKI_SUBSCRIBERS.UPDATE.status),
subjectAlternativeNames: validateAltNameField
.array()
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.subjectAlternativeNames),
ttl: z
.string()
.trim()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.ttl),
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.transform((arr) => Array.from(new Set(arr)))
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.keyUsages),
extendedKeyUsages: z
.nativeEnum(CertExtendedKeyUsage)
.array()
.transform((arr) => Array.from(new Set(arr)))
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages)
}),
response: {
200: sanitizedPkiSubscriber
}
},
handler: async (req) => {
const subscriber = await server.services.pkiSubscriber.updateSubscriber({
subscriberName: req.params.subscriberName,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: subscriber.projectId,
event: {
type: EventType.UPDATE_PKI_SUBSCRIBER,
metadata: {
pkiSubscriberId: subscriber.id,
caId: subscriber.caId ?? undefined,
name: subscriber.name,
commonName: subscriber.commonName,
ttl: subscriber.ttl,
subjectAlternativeNames: subscriber.subjectAlternativeNames,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
}
}
});
return subscriber;
}
});
server.route({
method: "DELETE",
url: "/:subscriberName",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Delete PKI Subscriber",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.DELETE.subscriberName)
}),
body: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.DELETE.projectId)
}),
response: {
200: sanitizedPkiSubscriber
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const subscriber = await server.services.pkiSubscriber.deleteSubscriber({
subscriberName: req.params.subscriberName,
projectId: req.body.projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: subscriber.projectId,
event: {
type: EventType.DELETE_PKI_SUBSCRIBER,
metadata: {
pkiSubscriberId: subscriber.id,
name: subscriber.name
}
}
});
return subscriber;
}
});
server.route({
method: "POST",
url: "/:subscriberName/issue-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Issue certificate",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName)
}),
body: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.projectId)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificateChain),
privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.privateKey),
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, subscriber } =
await server.services.pkiSubscriber.issueSubscriberCert({
subscriberName: req.params.subscriberName,
projectId: req.body.projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: subscriber.projectId,
event: {
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT,
metadata: {
subscriberId: subscriber.id,
name: subscriber.name,
serialNumber
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IssueCert,
distinctId: getTelemetryDistinctId(req),
properties: {
subscriberId: subscriber.id,
commonName: subscriber.commonName,
...req.auditLogInfo
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
privateKey,
serialNumber
};
}
});
server.route({
method: "POST",
url: "/:subscriberName/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Sign certificate",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.SIGN_CERT.subscriberName)
}),
body: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.projectId),
csr: z.string().trim().min(1).max(3000).describe(PKI_SUBSCRIBERS.SIGN_CERT.csr)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificateChain),
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, subscriber } =
await server.services.pkiSubscriber.signSubscriberCert({
subscriberName: req.params.subscriberName,
projectId: req.body.projectId,
csr: req.body.csr,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: subscriber.projectId,
event: {
type: EventType.SIGN_PKI_SUBSCRIBER_CERT,
metadata: {
subscriberId: subscriber.id,
name: subscriber.name,
serialNumber
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SignCert,
distinctId: getTelemetryDistinctId(req),
properties: {
subscriberId: subscriber.id,
commonName: subscriber.commonName,
...req.auditLogInfo
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
server.route({
method: "GET",
url: "/:subscriberName/certificates",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "List PKI Subscriber certificates",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName)
}),
querystring: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.LIST_CERTS.projectId),
offset: z.coerce.number().min(0).max(100).default(0).describe(PKI_SUBSCRIBERS.LIST_CERTS.offset),
limit: z.coerce.number().min(1).max(100).default(25).describe(PKI_SUBSCRIBERS.LIST_CERTS.limit)
}),
response: {
200: z.object({
certificates: z.array(CertificatesSchema),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { totalCount, certificates } = await server.services.pkiSubscriber.listSubscriberCerts({
subscriberName: req.params.subscriberName,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.LIST_PKI_SUBSCRIBER_CERTS,
metadata: {
subscriberId: req.params.subscriberName,
name: req.params.subscriberName,
projectId: req.query.projectId
}
}
});
return {
certificates,
totalCount
};
}
});
};

View File

@@ -346,7 +346,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore." "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
}) })
.optional() .optional()
.describe(PROJECTS.UPDATE.slug) .describe(PROJECTS.UPDATE.slug),
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -366,7 +367,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization, autoCapitalization: req.body.autoCapitalization,
hasDeleteProtection: req.body.hasDeleteProtection, hasDeleteProtection: req.body.hasDeleteProtection,
slug: req.body.slug slug: req.body.slug,
secretSharing: req.body.secretSharing
}, },
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -511,7 +513,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspace = await server.services.project.updateAuditLogsRetention({ const workspace = await server.services.project.updateAuditLogsRetention({
actorId: req.permission.id, actorId: req.permission.id,

View File

@@ -62,7 +62,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}), }),
body: z.object({ body: z.object({
hashedHex: z.string().min(1).optional(), hashedHex: z.string().min(1).optional(),
password: z.string().optional() password: z.string().optional(),
email: z.string().optional(),
hash: z.string().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -88,7 +90,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
sharedSecretId: req.params.id, sharedSecretId: req.params.id,
hashedHex: req.body.hashedHex, hashedHex: req.body.hashedHex,
password: req.body.password, password: req.body.password,
orgId: req.permission?.orgId orgId: req.permission?.orgId,
email: req.body.email,
hash: req.body.hash
}); });
if (sharedSecret.secret?.orgId) { if (sharedSecret.secret?.orgId) {
@@ -151,7 +155,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
secretValue: z.string(), secretValue: z.string(),
expiresAt: z.string(), expiresAt: z.string(),
expiresAfterViews: z.number().min(1).optional(), expiresAfterViews: z.number().min(1).optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
emails: z.string().email().array().max(100).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -10,6 +10,7 @@ import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router";
@@ -31,5 +32,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Vercel]: registerVercelSyncRouter, [SecretSync.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter, [SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.HCVault]: registerHCVaultSyncRouter, [SecretSync.HCVault]: registerHCVaultSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter [SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter
}; };

View File

@@ -0,0 +1,17 @@
import {
CreateOCIVaultSyncSchema,
OCIVaultSyncSchema,
UpdateOCIVaultSyncSchema
} from "@app/services/secret-sync/oci-vault";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.OCIVault,
server,
responseSchema: OCIVaultSyncSchema,
createSchema: CreateOCIVaultSyncSchema,
updateSchema: UpdateOCIVaultSyncSchema
});

View File

@@ -24,6 +24,7 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/services/secret-sync/oci-vault";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -43,7 +44,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
VercelSyncSchema, VercelSyncSchema,
WindmillSyncSchema, WindmillSyncSchema,
HCVaultSyncSchema, HCVaultSyncSchema,
TeamCitySyncSchema TeamCitySyncSchema,
OCIVaultSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -60,7 +62,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
VercelSyncListItemSchema, VercelSyncListItemSchema,
WindmillSyncListItemSchema, WindmillSyncListItemSchema,
HCVaultSyncListItemSchema, HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -24,6 +24,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema";
import { ProjectFilterType } from "@app/services/project/project-types"; import { ProjectFilterType } from "@app/services/project/project-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -490,6 +491,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/:projectId/pki-subscribers",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_PKI_SUBSCRIBERS.projectId)
}),
response: {
200: z.object({
subscribers: z.array(sanitizedPkiSubscriber)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const subscribers = await server.services.project.listProjectPkiSubscribers({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
projectId: req.params.projectId
});
return { subscribers };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:projectId/certificate-templates", url: "/:projectId/certificate-templates",
@@ -628,6 +661,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHosts],
params: z.object({ params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId) projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId)
}), }),
@@ -666,6 +701,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SshHostGroups],
params: z.object({ params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId) projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId)
}), }),

View File

@@ -16,7 +16,8 @@ export enum AppConnection {
Auth0 = "auth0", Auth0 = "auth0",
HCVault = "hashicorp-vault", HCVault = "hashicorp-vault",
LDAP = "ldap", LDAP = "ldap",
TeamCity = "teamcity" TeamCity = "teamcity",
OCI = "oci"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@@ -53,6 +53,7 @@ import {
} from "./humanitec"; } from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { import {
getTeamCityConnectionListItem, getTeamCityConnectionListItem,
@@ -91,7 +92,8 @@ export const listAppConnectionOptions = () => {
getAuth0ConnectionListItem(), getAuth0ConnectionListItem(),
getHCVaultConnectionListItem(), getHCVaultConnectionListItem(),
getLdapConnectionListItem(), getLdapConnectionListItem(),
getTeamCityConnectionListItem() getTeamCityConnectionListItem(),
getOCIConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -160,7 +162,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -176,6 +179,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case GitHubConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth:
return "OAuth"; return "OAuth";
case AwsConnectionMethod.AccessKey: case AwsConnectionMethod.AccessKey:
case OCIConnectionMethod.AccessKey:
return "Access Key"; return "Access Key";
case AwsConnectionMethod.AssumeRole: case AwsConnectionMethod.AssumeRole:
return "Assume Role"; return "Assume Role";
@@ -250,5 +254,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Auth0]: platformManagedCredentialsNotSupported, [AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.HCVault]: platformManagedCredentialsNotSupported, [AppConnection.HCVault]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported [AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
[AppConnection.OCI]: platformManagedCredentialsNotSupported
}; };

View File

@@ -18,5 +18,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Auth0]: "Auth0", [AppConnection.Auth0]: "Auth0",
[AppConnection.HCVault]: "Hashicorp Vault", [AppConnection.HCVault]: "Hashicorp Vault",
[AppConnection.LDAP]: "LDAP", [AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity" [AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI"
}; };

View File

@@ -49,6 +49,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateOCIConnectionCredentialsSchema } from "./oci";
import { ociConnectionService } from "./oci/oci-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
@@ -85,7 +87,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema, [AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema, [AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema, [AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema [AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -464,6 +467,7 @@ export const appConnectionServiceFactory = ({
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
hcvault: hcVaultConnectionService(connectAppConnectionById), hcvault: hcVaultConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById) teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById)
}; };
}; };

View File

@@ -76,6 +76,12 @@ import {
TValidateLdapConnectionCredentialsSchema TValidateLdapConnectionCredentialsSchema
} from "./ldap"; } from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TOCIConnection,
TOCIConnectionConfig,
TOCIConnectionInput,
TValidateOCIConnectionCredentialsSchema
} from "./oci";
import { import {
TPostgresConnection, TPostgresConnection,
TPostgresConnectionInput, TPostgresConnectionInput,
@@ -125,6 +131,7 @@ export type TAppConnection = { id: string } & (
| THCVaultConnection | THCVaultConnection
| TLdapConnection | TLdapConnection
| TTeamCityConnection | TTeamCityConnection
| TOCIConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -150,6 +157,7 @@ export type TAppConnectionInput = { id: string } & (
| THCVaultConnectionInput | THCVaultConnectionInput
| TLdapConnectionInput | TLdapConnectionInput
| TTeamCityConnectionInput | TTeamCityConnectionInput
| TOCIConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -180,7 +188,8 @@ export type TAppConnectionConfig =
| TAuth0ConnectionConfig | TAuth0ConnectionConfig
| THCVaultConnectionConfig | THCVaultConnectionConfig
| TLdapConnectionConfig | TLdapConnectionConfig
| TTeamCityConnectionConfig; | TTeamCityConnectionConfig
| TOCIConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -200,7 +209,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateAuth0ConnectionCredentialsSchema | TValidateAuth0ConnectionCredentialsSchema
| TValidateHCVaultConnectionCredentialsSchema | TValidateHCVaultConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema | TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema; | TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

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

View File

@@ -0,0 +1,3 @@
export enum OCIConnectionMethod {
AccessKey = "access-key"
}

View File

@@ -0,0 +1,139 @@
import { common, identity, keymanagement } from "oci-sdk";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { OCIConnectionMethod } from "./oci-connection-enums";
import { TOCIConnection, TOCIConnectionConfig } from "./oci-connection-types";
export const getOCIProvider = async (config: TOCIConnectionConfig) => {
const {
credentials: { fingerprint, privateKey, region, tenancyOcid, userOcid }
} = config;
const provider = new common.SimpleAuthenticationDetailsProvider(
tenancyOcid,
userOcid,
fingerprint,
privateKey,
null,
common.Region.fromRegionId(region)
);
return provider;
};
export const getOCIConnectionListItem = () => {
return {
name: "OCI" as const,
app: AppConnection.OCI as const,
methods: Object.values(OCIConnectionMethod) as [OCIConnectionMethod.AccessKey]
};
};
export const validateOCIConnectionCredentials = async (config: TOCIConnectionConfig) => {
const provider = await getOCIProvider(config);
try {
const identityClient = new identity.IdentityClient({
authenticationDetailsProvider: provider
});
// Get user details - a lightweight call that validates all credentials
await identityClient.getUser({ userId: config.credentials.userOcid });
} catch (error: unknown) {
if (error instanceof Error) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};
export const listOCICompartments = async (appConnection: TOCIConnection) => {
const provider = await getOCIProvider(appConnection);
const identityClient = new identity.IdentityClient({ authenticationDetailsProvider: provider });
const keyManagementClient = new keymanagement.KmsVaultClient({
authenticationDetailsProvider: provider
});
const rootCompartment = await identityClient
.getTenancy({
tenancyId: appConnection.credentials.tenancyOcid
})
.then((response) => ({
...response.tenancy,
id: appConnection.credentials.tenancyOcid,
name: response.tenancy.name ? `${response.tenancy.name} (root)` : "root"
}));
const compartments = await identityClient.listCompartments({
compartmentId: appConnection.credentials.tenancyOcid,
compartmentIdInSubtree: true,
accessLevel: identity.requests.ListCompartmentsRequest.AccessLevel.Any,
lifecycleState: identity.models.Compartment.LifecycleState.Active
});
const allCompartments = [rootCompartment, ...compartments.items];
const filteredCompartments = [];
for await (const compartment of allCompartments) {
try {
// Check if user can list vaults in this compartment
await keyManagementClient.listVaults({
compartmentId: compartment.id,
limit: 1
});
filteredCompartments.push(compartment);
} catch (error) {
// Do nothing
}
}
return filteredCompartments;
};
export const listOCIVaults = async (appConnection: TOCIConnection, compartmentOcid: string) => {
const provider = await getOCIProvider(appConnection);
const keyManagementClient = new keymanagement.KmsVaultClient({
authenticationDetailsProvider: provider
});
const vaults = await keyManagementClient.listVaults({
compartmentId: compartmentOcid
});
return vaults.items.filter((v) => v.lifecycleState === keymanagement.models.Vault.LifecycleState.Active);
};
export const listOCIVaultKeys = async (appConnection: TOCIConnection, compartmentOcid: string, vaultOcid: string) => {
const provider = await getOCIProvider(appConnection);
const kmsVaultClient = new keymanagement.KmsVaultClient({
authenticationDetailsProvider: provider
});
const vault = await kmsVaultClient.getVault({
vaultId: vaultOcid
});
const keyManagementClient = new keymanagement.KmsManagementClient({
authenticationDetailsProvider: provider
});
keyManagementClient.endpoint = vault.vault.managementEndpoint;
const keys = await keyManagementClient.listKeys({
compartmentId: compartmentOcid
});
return keys.items.filter((v) => v.lifecycleState === keymanagement.models.KeySummary.LifecycleState.Enabled);
};

View File

@@ -0,0 +1,65 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { OCIConnectionMethod } from "./oci-connection-enums";
export const OCIConnectionAccessTokenCredentialsSchema = z.object({
userOcid: z.string().trim().min(1, "User OCID required").describe(AppConnections.CREDENTIALS.OCI.userOcid),
tenancyOcid: z.string().trim().min(1, "Tenancy OCID required").describe(AppConnections.CREDENTIALS.OCI.tenancyOcid),
region: z.string().trim().min(1, "Region required").describe(AppConnections.CREDENTIALS.OCI.region),
fingerprint: z.string().trim().min(1, "Fingerprint required").describe(AppConnections.CREDENTIALS.OCI.fingerprint),
privateKey: z.string().trim().min(1, "Private Key required").describe(AppConnections.CREDENTIALS.OCI.privateKey)
});
const BaseOCIConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.OCI) });
export const OCIConnectionSchema = BaseOCIConnectionSchema.extend({
method: z.literal(OCIConnectionMethod.AccessKey),
credentials: OCIConnectionAccessTokenCredentialsSchema
});
export const SanitizedOCIConnectionSchema = z.discriminatedUnion("method", [
BaseOCIConnectionSchema.extend({
method: z.literal(OCIConnectionMethod.AccessKey),
credentials: OCIConnectionAccessTokenCredentialsSchema.pick({
userOcid: true,
tenancyOcid: true,
region: true,
fingerprint: true
})
})
]);
export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(OCIConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.OCI).method),
credentials: OCIConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.OCI).credentials
)
})
]);
export const CreateOCIConnectionSchema = ValidateOCIConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.OCI)
);
export const UpdateOCIConnectionSchema = z
.object({
credentials: OCIConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.OCI).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI));
export const OCIConnectionListItemSchema = z.object({
name: z.literal("OCI"),
app: z.literal(AppConnection.OCI),
methods: z.nativeEnum(OCIConnectionMethod).array()
});

View File

@@ -0,0 +1,70 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
import { TOCIConnection } from "./oci-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TOCIConnection>;
type TListOCIVaultsDTO = {
connectionId: string;
compartmentOcid: string;
};
type TListOCIVaultKeysDTO = {
connectionId: string;
compartmentOcid: string;
vaultOcid: string;
};
export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listCompartments = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try {
const compartments = await listOCICompartments(appConnection);
return compartments;
} catch (error) {
logger.error(error, "Failed to establish connection with OCI");
return [];
}
};
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try {
const vaults = await listOCIVaults(appConnection, compartmentOcid);
return vaults;
} catch (error) {
logger.error(error, "Failed to establish connection with OCI");
return [];
}
};
const listVaultKeys = async (
{ connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO,
actor: OrgServiceActor
) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try {
const keys = await listOCIVaultKeys(appConnection, compartmentOcid, vaultOcid);
return keys;
} catch (error) {
logger.error(error, "Failed to establish connection with OCI");
return [];
}
};
return {
listCompartments,
listVaults,
listVaultKeys
};
};

View File

@@ -0,0 +1,22 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateOCIConnectionSchema,
OCIConnectionSchema,
ValidateOCIConnectionCredentialsSchema
} from "./oci-connection-schemas";
export type TOCIConnection = z.infer<typeof OCIConnectionSchema>;
export type TOCIConnectionInput = z.infer<typeof CreateOCIConnectionSchema> & {
app: AppConnection.OCI;
};
export type TValidateOCIConnectionCredentialsSchema = typeof ValidateOCIConnectionCredentialsSchema;
export type TOCIConnectionConfig = DiscriminativePick<TOCIConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};

View File

@@ -1169,7 +1169,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.requireTemplateForIssuance && !certificateTemplate) { if (ca.requireTemplateForIssuance && !certificateTemplate) {
throw new BadRequestError({ message: "Certificate template is required for issuance" }); throw new BadRequestError({ message: "Certificate template is required for issuance" });
@@ -1520,7 +1520,7 @@ export const certificateAuthorityServiceFactory = ({
); );
} }
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.requireTemplateForIssuance && !certificateTemplate) { if (ca.requireTemplateForIssuance && !certificateTemplate) {
throw new BadRequestError({ message: "Certificate template is required for issuance" }); throw new BadRequestError({ message: "Certificate template is required for issuance" });

View File

@@ -10,6 +10,18 @@ const isValidDate = (dateString: string) => {
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" }); export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
export const validateAltNameField = z
.string()
.trim()
.refine(
(name) => {
return isFQDN(name) || z.string().email().safeParse(name).success || isValidIp(name);
},
{
message: "SAN must be a valid hostname, email address, or IP address"
}
);
export const validateAltNamesField = z export const validateAltNamesField = z
.string() .string()
.trim() .trim()

View File

@@ -44,8 +44,27 @@ export const certificateDALFactory = (db: TDbClient) => {
} }
}; };
const countCertificatesForPkiSubscriber = async (subscriberId: string) => {
try {
interface CountResult {
count: string;
}
const query = db
.replicaNode()(TableName.Certificate)
.where(`${TableName.Certificate}.pkiSubscriberId`, subscriberId);
const count = await query.count("*").first();
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count all subscriber certificates" });
}
};
return { return {
...certificateOrm, ...certificateOrm,
countCertificatesInProject countCertificatesInProject,
countCertificatesForPkiSubscriber
}; };
}; };

View File

@@ -105,7 +105,7 @@ export const buildCertificateChain = async ({
kmsService, kmsService,
kmsId kmsId
}: TBuildCertificateChainDTO) => { }: TBuildCertificateChainDTO) => {
if (!encryptedCertificateChain && (!caCert || !caCertChain)) { if (!encryptedCertificateChain && !caCert) {
return null; return null;
} }

View File

@@ -29,6 +29,7 @@ import {
TGetCertPrivateKeyDTO, TGetCertPrivateKeyDTO,
TRevokeCertDTO TRevokeCertDTO
} from "./certificate-types"; } from "./certificate-types";
import { NotFoundError } from "@app/lib/errors";
type TCertificateServiceFactoryDep = { type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
@@ -337,18 +338,27 @@ export const certificateServiceFactory = ({
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
}); });
const { certPrivateKey } = await getCertificateCredentials({ let privateKey: string | null = null;
certId: cert.id, try {
projectId: ca.projectId, const { certPrivateKey } = await getCertificateCredentials({
certificateSecretDAL, certId: cert.id,
projectDAL, projectId: ca.projectId,
kmsService certificateSecretDAL,
}); projectDAL,
kmsService
});
privateKey = certPrivateKey;
} catch (e) {
// Skip NotFound errors but throw all others
if (!(e instanceof NotFoundError)) {
throw e;
}
}
return { return {
certificate, certificate,
certificateChain, certificateChain,
privateKey: certPrivateKey, privateKey,
serialNumber, serialNumber,
cert, cert,
ca ca

View File

@@ -36,6 +36,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`)
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
@@ -46,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOciAuth).as("accessTokenTrustedIpsOci"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
@@ -63,6 +65,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws, trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
trustedIpsOciAuth: doc.accessTokenTrustedIpsOci,
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt, trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,

View File

@@ -182,6 +182,7 @@ export const identityAccessTokenServiceFactory = ({
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth, [IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth, [IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth, [IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,

View File

@@ -4,8 +4,14 @@ import https from "https";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import {
OrgPermissionGatewayActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
@@ -13,6 +19,7 @@ import {
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { withGatewayProxy } from "@app/lib/gateway";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -43,6 +50,8 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayService: TGatewayServiceFactory;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
}; };
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>; export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
@@ -53,8 +62,45 @@ export const identityKubernetesAuthServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
licenseService, licenseService,
gatewayService,
gatewayDAL,
kmsService kmsService
}: TIdentityKubernetesAuthServiceFactoryDep) => { }: TIdentityKubernetesAuthServiceFactoryDep) => {
const $gatewayProxyWrapper = async <T>(
inputs: {
gatewayId: string;
targetHost: string;
targetPort: number;
},
gatewayCallback: (host: string, port: number) => Promise<T>
): Promise<T> => {
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
const callbackResult = await withGatewayProxy(
async (port) => {
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
const res = await gatewayCallback("https://localhost", port);
return res;
},
{
targetHost: inputs.targetHost,
targetPort: inputs.targetPort,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
return callbackResult;
};
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) { if (!identityKubernetesAuth) {
@@ -92,46 +138,65 @@ export const identityKubernetesAuthServiceFactory = ({
tokenReviewerJwt = serviceAccountJwt; tokenReviewerJwt = serviceAccountJwt;
} }
const { data } = await axios const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
.post<TCreateTokenReviewResponse>( const baseUrl = port ? `${host}:${port}` : host;
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) { const res = await axios
throw new UnauthorizedError({ .post<TCreateTokenReviewResponse>(
message, `${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
name: "KubernetesTokenReviewRequestError" {
}); apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
} }
} }
} throw err;
throw err; });
});
return res.data;
};
const [k8sHost, k8sPort] = identityKubernetesAuth.kubernetesHost.split(":");
const data = identityKubernetesAuth.gatewayId
? await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443
},
tokenReviewCallback
)
: await tokenReviewCallback();
if ("error" in data.status) if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" }); throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
@@ -222,6 +287,7 @@ export const identityKubernetesAuthServiceFactory = ({
const attachKubernetesAuth = async ({ const attachKubernetesAuth = async ({
identityId, identityId,
gatewayId,
kubernetesHost, kubernetesHost,
caCert, caCert,
tokenReviewerJwt, tokenReviewerJwt,
@@ -280,6 +346,27 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
@@ -296,6 +383,7 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
gatewayId,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt
? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob ? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob
@@ -318,6 +406,7 @@ export const identityKubernetesAuthServiceFactory = ({
allowedNamespaces, allowedNamespaces,
allowedNames, allowedNames,
allowedAudience, allowedAudience,
gatewayId,
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
@@ -373,11 +462,33 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const updateQuery: TIdentityKubernetesAuthsUpdate = { const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost, kubernetesHost,
allowedNamespaces, allowedNamespaces,
allowedNames, allowedNames,
allowedAudience, allowedAudience,
gatewayId,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,

View File

@@ -13,6 +13,7 @@ export type TAttachKubernetesAuthDTO = {
allowedNamespaces: string; allowedNamespaces: string;
allowedNames: string; allowedNames: string;
allowedAudience: string; allowedAudience: string;
gatewayId?: string | null;
accessTokenTTL: number; accessTokenTTL: number;
accessTokenMaxTTL: number; accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number; accessTokenNumUsesLimit: number;
@@ -28,6 +29,7 @@ export type TUpdateKubernetesAuthDTO = {
allowedNamespaces?: string; allowedNamespaces?: string;
allowedNames?: string; allowedNames?: string;
allowedAudience?: string; allowedAudience?: string;
gatewayId?: string | null;
accessTokenTTL?: number; accessTokenTTL?: number;
accessTokenMaxTTL?: number; accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number; accessTokenNumUsesLimit?: number;

View File

@@ -0,0 +1,9 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityOciAuthDALFactory = ReturnType<typeof identityOciAuthDALFactory>;
export const identityOciAuthDALFactory = (db: TDbClient) => {
return ormify(db, TableName.IdentityOciAuth);
};

View File

@@ -0,0 +1,368 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { AxiosError } from "axios";
import jwt from "jsonwebtoken";
import RE2 from "re2";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityOciAuthDALFactory } from "./identity-oci-auth-dal";
import {
TAttachOciAuthDTO,
TGetOciAuthDTO,
TLoginOciAuthDTO,
TOciGetUserResponse,
TRevokeOciAuthDTO,
TUpdateOciAuthDTO
} from "./identity-oci-auth-types";
type TIdentityOciAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
identityOciAuthDAL: Pick<TIdentityOciAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
export type TIdentityOciAuthServiceFactory = ReturnType<typeof identityOciAuthServiceFactory>;
export const identityOciAuthServiceFactory = ({
identityAccessTokenDAL,
identityOciAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
}: TIdentityOciAuthServiceFactoryDep) => {
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (!identityOciAuth) {
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId });
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"
});
}
const { data } = await request
.get<TOciGetUserResponse>(`https://${headers.host}/20160918/users/${userOcid}`, {
headers
})
.catch((err: AxiosError) => {
logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud");
throw err;
});
if (data.compartmentId !== identityOciAuth.tenancyOcid) {
throw new UnauthorizedError({
message: "Access denied: OCI account isn't part of tenancy."
});
}
if (identityOciAuth.allowedUsernames) {
const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: OCI account username not allowed."
});
}
// Generate the token
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOciAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OCI_AUTH
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityOciAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return {
identityOciAuth,
accessToken,
identityAccessToken,
identityMembershipOrg
};
};
const attachOciAuth = async ({
identityId,
tenancyOcid,
allowedUsernames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId,
isActorSuperAdmin
}: TAttachOciAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "Failed to add OCI Auth to already configured identity"
});
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const identityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
const doc = await identityOciAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
type: "iam",
tenancyOcid,
allowedUsernames,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
return doc;
});
return { ...identityOciAuth, orgId: identityMembershipOrg.orgId };
};
const updateOciAuth = async ({
identityId,
tenancyOcid,
allowedUsernames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new NotFoundError({
message: "The identity does not have OCI Auth attached"
});
}
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityOciAuth.accessTokenTTL) > (accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updatedOciAuth = await identityOciAuthDAL.updateById(identityOciAuth.id, {
tenancyOcid,
allowedUsernames,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return { ...updatedOciAuth, orgId: identityMembershipOrg.orgId };
};
const getOciAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "The identity does not have OCI Auth attached"
});
}
const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...ociIdentityAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityOciAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "The identity does not have OCI auth"
});
}
const { permission, membership } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke OCI auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx);
return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityOciAuth;
};
return {
login,
attachOciAuth,
updateOciAuth,
getOciAuth,
revokeIdentityOciAuth
};
};

View File

@@ -0,0 +1,53 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginOciAuthDTO = {
identityId: string;
userOcid: string;
headers: {
authorization: string;
host: string;
"x-date": string;
};
};
export type TAttachOciAuthDTO = {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateOciAuthDTO = {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetOciAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeOciAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TOciGetUserResponse = {
email: string;
emailVerified: boolean;
timeModified: string;
isMfaActivated: boolean;
id: string;
compartmentId: string;
name: string;
timeCreated: string;
freeformTags: { [key: string]: string };
lifecycleState: string;
};

View File

@@ -0,0 +1,32 @@
import RE2 from "re2";
import { z } from "zod";
const usernameSchema = z
.string()
.min(1, "Username cannot be empty")
.refine((val) => new RE2("^[a-zA-Z0-9._@-]+$").test(val), "Invalid OCI username format");
export const validateUsernames = z
.string()
.trim()
.max(500, "Input exceeds the maximum limit of 500 characters")
.nullish()
.transform((val) => {
if (!val) return [];
return val
.split(",")
.map((s) => s.trim())
.filter(Boolean);
})
.refine((arr) => arr.every((name) => usernameSchema.safeParse(name).success), {
message: "One or more usernames are invalid"
})
.transform((arr) => (arr.length > 0 ? arr.join(", ") : null));
export const validateTenancy = z
.string()
.trim()
.min(1, "Tenancy OCID cannot be empty.")
.refine(
(val) => new RE2("^ocid1\\.tenancy\\.oc1\\..+$").test(val),
"Invalid Tenancy OCID format. Must start with ocid1.tenancy.oc1."
);

View File

@@ -8,6 +8,7 @@ import {
TIdentityAzureAuths, TIdentityAzureAuths,
TIdentityGcpAuths, TIdentityGcpAuths,
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityTokenAuths, TIdentityTokenAuths,
TIdentityUniversalAuths TIdentityUniversalAuths
@@ -66,6 +67,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin(
TableName.IdentityOciAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin( .leftJoin(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityProjectMembership}.identityId`,
@@ -107,6 +113,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth) db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
@@ -270,6 +277,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>( .leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
@@ -309,6 +321,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth) db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
@@ -336,6 +349,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -356,6 +370,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId tokenId

View File

@@ -5,6 +5,7 @@ export const buildAuthMethods = ({
gcpId, gcpId,
awsId, awsId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -15,6 +16,7 @@ export const buildAuthMethods = ({
gcpId?: string; gcpId?: string;
awsId?: string; awsId?: string;
kubernetesId?: string; kubernetesId?: string;
ociId?: string;
oidcId?: string; oidcId?: string;
azureId?: string; azureId?: string;
tokenId?: string; tokenId?: string;
@@ -26,6 +28,7 @@ export const buildAuthMethods = ({
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null], ...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
...[awsId ? IdentityAuthMethod.AWS_AUTH : null], ...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[ociId ? IdentityAuthMethod.OCI_AUTH : null],
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null], ...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null], ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],

View File

@@ -8,6 +8,7 @@ import {
TIdentityGcpAuths, TIdentityGcpAuths,
TIdentityJwtAuths, TIdentityJwtAuths,
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOrgMemberships, TIdentityOrgMemberships,
TIdentityTokenAuths, TIdentityTokenAuths,
@@ -62,6 +63,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>( .leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
@@ -95,6 +101,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -186,6 +193,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>( .leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
@@ -226,6 +238,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -269,6 +282,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
gcpId, gcpId,
jwtId, jwtId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -301,6 +315,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -401,6 +416,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin(
TableName.IdentityOciAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin( .leftJoin(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
@@ -441,6 +461,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -485,6 +506,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
gcpId, gcpId,
jwtId, jwtId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -517,6 +539,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,

View File

@@ -106,18 +106,29 @@ export const identityServiceFactory = ({
}, },
tx tx
); );
let insertedMetadata: Array<{
id: string;
key: string;
value: string;
}> = [];
if (metadata && metadata.length) { if (metadata && metadata.length) {
await identityMetadataDAL.insertMany( const rowsToInsert = metadata.map(({ key, value }) => ({
metadata.map(({ key, value }) => ({ identityId: newIdentity.id,
identityId: newIdentity.id, orgId,
orgId, key,
key, value
value }));
})),
tx insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
);
} }
return { ...newIdentity, authMethods: [] };
return {
...newIdentity,
authMethods: [],
metadata: insertedMetadata
};
}); });
await licenseService.updateSubscriptionOrgMemberCount(orgId); await licenseService.updateSubscriptionOrgMemberCount(orgId);
@@ -189,21 +200,31 @@ export const identityServiceFactory = ({
tx tx
); );
} }
let insertedMetadata: Array<{
id: string;
key: string;
value: string;
}> = [];
if (metadata) { if (metadata) {
await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx); await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx);
if (metadata.length) { if (metadata.length) {
await identityMetadataDAL.insertMany( const rowsToInsert = metadata.map(({ key, value }) => ({
metadata.map(({ key, value }) => ({ identityId: newIdentity.id,
identityId: newIdentity.id, orgId: identityOrgMembership.orgId,
orgId: identityOrgMembership.orgId, key,
key, value
value }));
})),
tx insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
);
} }
} }
return newIdentity;
return {
...newIdentity,
metadata: insertedMetadata
};
}); });
return { ...identity, orgId: identityOrgMembership.orgId }; return { ...identity, orgId: identityOrgMembership.orgId };
@@ -224,6 +245,7 @@ export const identityServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return identity; return identity;
}; };

View File

@@ -18,5 +18,13 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
privilegeUpgradeInitiatedByUsername: true, privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true, privilegeUpgradeInitiatedAt: true,
bypassOrgAuthEnabled: true, bypassOrgAuthEnabled: true,
userTokenExpiration: true userTokenExpiration: true,
secretsProductEnabled: true,
pkiProductEnabled: true,
kmsProductEnabled: true,
sshProductEnabled: true,
scannerProductEnabled: true,
shareSecretsProductEnabled: true,
maxSharedSecretLifetime: true,
maxSharedSecretViewLimit: true
}); });

View File

@@ -355,7 +355,15 @@ export const orgServiceFactory = ({
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization, allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled, bypassOrgAuthEnabled,
userTokenExpiration userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled,
maxSharedSecretLifetime,
maxSharedSecretViewLimit
} }
}: TUpdateOrgDTO) => { }: TUpdateOrgDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -457,7 +465,15 @@ export const orgServiceFactory = ({
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization, allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled, bypassOrgAuthEnabled,
userTokenExpiration userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled,
maxSharedSecretLifetime,
maxSharedSecretViewLimit
}); });
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org; return org;

View File

@@ -75,6 +75,14 @@ export type TUpdateOrgDTO = {
allowSecretSharingOutsideOrganization: boolean; allowSecretSharingOutsideOrganization: boolean;
bypassOrgAuthEnabled: boolean; bypassOrgAuthEnabled: boolean;
userTokenExpiration: string; userTokenExpiration: string;
secretsProductEnabled: boolean;
pkiProductEnabled: boolean;
kmsProductEnabled: boolean;
sshProductEnabled: boolean;
scannerProductEnabled: boolean;
shareSecretsProductEnabled: boolean;
maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null;
}>; }>;
} & TOrgPermission; } & TOrgPermission;

View File

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

View File

@@ -0,0 +1,14 @@
import { PkiSubscribersSchema } from "@app/db/schemas";
export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
id: true,
projectId: true,
caId: true,
name: true,
commonName: true,
status: true,
subjectAlternativeNames: true,
ttl: true,
keyUsages: true,
extendedKeyUsages: true
});

View File

@@ -0,0 +1,805 @@
/* eslint-disable no-bitwise */
import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
} from "@app/services/certificate/certificate-types";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import {
createSerialNumber,
getCaCertChain,
getCaCredentials,
keyAlgorithmToAlgCfg,
parseDistinguishedName
} from "@app/services/certificate-authority/certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import {
PkiSubscriberStatus,
TCreatePkiSubscriberDTO,
TDeletePkiSubscriberDTO,
TGetPkiSubscriberDTO,
TIssuePkiSubscriberCertDTO,
TListPkiSubscriberCertsDTO,
TSignPkiSubscriberCertDTO,
TUpdatePkiSubscriberDTO
} from "./pki-subscriber-types";
type TPkiSubscriberServiceFactoryDep = {
pkiSubscriberDAL: Pick<
TPkiSubscriberDALFactory,
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne"
>;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
export const pkiSubscriberServiceFactory = ({
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService,
permissionService
}: TPkiSubscriberServiceFactoryDep) => {
const createSubscriber = async ({
name,
commonName,
status,
caId,
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreatePkiSubscriberDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.Create,
subject(ProjectPermissionSub.PkiSubscribers, {
name
})
);
const newSubscriber = await pkiSubscriberDAL.create({
caId,
projectId,
name,
commonName,
status,
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages
});
return newSubscriber;
};
const getSubscriber = async ({
subscriberName,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetPkiSubscriberDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.Read,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
return subscriber;
};
const updateSubscriber = async ({
subscriberName,
projectId,
name,
commonName,
status,
caId,
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdatePkiSubscriberDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.Edit,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, {
caId,
name,
commonName,
status,
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages
});
return updatedSubscriber;
};
const deleteSubscriber = async ({
subscriberName,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TDeletePkiSubscriberDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.Delete,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
await pkiSubscriberDAL.deleteById(subscriber.id);
return subscriber;
};
const issueSubscriberCert = async ({
subscriberName,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TIssuePkiSubscriberCertDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.IssueCert,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
throw new BadRequestError({ message: "Subscriber is not active" });
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.requireTemplateForIssuance) {
throw new BadRequestError({ message: "Certificate template is required for issuance" });
}
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${subscriber.commonName}`,
keys: leafKeys,
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
];
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
if (subscriber.extendedKeyUsages.length) {
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
true
);
extensions.push(extendedKeyUsagesExtension);
}
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
}
if (isFQDN(altName, { allow_wildcard: true })) {
return { type: "dns", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
caCertId: caCert.id,
pkiSubscriberId: subscriber.id,
status: CertStatus.ACTIVE,
friendlyName: subscriber.commonName,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames.join(","),
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
});
return {
certificate: leafCert.toString("pem"),
certificateChain: certificateChainPem,
issuingCaCertificate,
privateKey: skLeaf,
serialNumber,
ca,
subscriber
};
};
const signSubscriberCert = async ({
subscriberName,
projectId,
csr,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TSignPkiSubscriberCertDTO) => {
const appCfg = getConfig();
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.IssueCert,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
throw new BadRequestError({ message: "Subscriber is not active" });
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.requireTemplateForIssuance) {
throw new BadRequestError({ message: "Certificate template is required for issuance" });
}
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const dn = parseDistinguishedName(csrObj.subject);
const cn = dn.commonName;
if (cn !== subscriber.commonName) {
throw new BadRequestError({ message: "Common name (CN) in the CSR does not match the subscriber's common name" });
}
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
];
// handle key usages
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
let csrKeyUsages: CertKeyUsage[] = [];
if (csrKeyUsageExtension) {
csrKeyUsages = Object.values(CertKeyUsage).filter(
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
);
}
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
if (csrKeyUsages.some((keyUsage) => !selectedKeyUsages.includes(keyUsage))) {
throw new BadRequestError({
message: "Invalid key usage value based on subscriber's specified key usages"
});
}
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
// handle extended key usages
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
let csrExtendedKeyUsages: CertExtendedKeyUsage[] = [];
if (csrExtendedKeyUsageExtension) {
csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
);
}
const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[];
if (csrExtendedKeyUsages.some((eku) => !selectedExtendedKeyUsages.includes(eku))) {
throw new BadRequestError({
message: "Invalid extended key usage value based on subscriber's specified extended key usages"
});
}
if (selectedExtendedKeyUsages.length) {
extensions.push(
new x509.ExtendedKeyUsageExtension(
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
true
)
);
}
// attempt to read from CSR if altNames is not explicitly provided
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns")
.map((name) => ({
type: name.type as "email" | "dns",
value: name.value
}));
}
if (
altNamesArray
.map((altName) => altName.value)
.some((altName) => !subscriber.subjectAlternativeNames.includes(altName))
) {
throw new BadRequestError({
message: "Invalid subject alternative name based on subscriber's specified subject alternative names"
});
}
if (altNamesArray.length) {
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: ca.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
caCertId: caCert.id,
pkiSubscriberId: subscriber.id,
status: CertStatus.ACTIVE,
friendlyName: subscriber.commonName,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames.join(","),
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
return cert;
});
return {
certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,
ca,
commonName: subscriber.commonName,
subscriber
};
};
const listSubscriberCerts = async ({
subscriberName,
projectId,
offset,
limit,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TListPkiSubscriberCertsDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.ListCerts,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
const certificates = await certificateDAL.find(
{
pkiSubscriberId: subscriber.id
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
const count = await certificateDAL.countCertificatesForPkiSubscriber(subscriber.id);
return {
certificates,
totalCount: count
};
};
return {
createSubscriber,
getSubscriber,
updateSubscriber,
deleteSubscriber,
issueSubscriberCert,
signSubscriberCert,
listSubscriberCerts
};
};

View File

@@ -0,0 +1,54 @@
import { TProjectPermission } from "@app/lib/types";
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
export enum PkiSubscriberStatus {
ACTIVE = "active",
DISABLED = "disabled"
}
export type TCreatePkiSubscriberDTO = {
caId: string;
name: string;
commonName: string;
status: PkiSubscriberStatus;
ttl: string;
subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
} & TProjectPermission;
export type TGetPkiSubscriberDTO = {
subscriberName: string;
} & TProjectPermission;
export type TUpdatePkiSubscriberDTO = {
subscriberName: string;
caId?: string;
name?: string;
commonName?: string;
status?: PkiSubscriberStatus;
ttl?: string;
subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & TProjectPermission;
export type TDeletePkiSubscriberDTO = {
subscriberName: string;
} & TProjectPermission;
export type TIssuePkiSubscriberCertDTO = {
subscriberName: string;
} & TProjectPermission;
export type TSignPkiSubscriberCertDTO = {
subscriberName: string;
csr: string;
} & TProjectPermission;
export type TListPkiSubscriberCertsDTO = {
subscriberName: string;
offset: number;
limit: number;
} & TProjectPermission;

View File

@@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
ProjectPermissionSub ProjectPermissionSub
@@ -35,6 +36,7 @@ import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TQueueServiceFactory } from "@app/queue"; import { TQueueServiceFactory } from "@app/queue";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateDALFactory } from "../certificate/certificate-dal";
@@ -86,6 +88,7 @@ import {
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertificateTemplatesDTO, TListProjectCertificateTemplatesDTO,
TListProjectCertsDTO, TListProjectCertsDTO,
TListProjectPkiSubscribersDTO,
TListProjectsDTO, TListProjectsDTO,
TListProjectSshCasDTO, TListProjectSshCasDTO,
TListProjectSshCertificatesDTO, TListProjectSshCertificatesDTO,
@@ -145,6 +148,7 @@ type TProjectServiceFactoryDep = {
"findById" | "findByIdWithWorkflowIntegrationDetails" "findById" | "findByIdWithWorkflowIntegrationDetails"
>; >;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">; certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">; certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
@@ -207,6 +211,7 @@ export const projectServiceFactory = ({
certificateTemplateDAL, certificateTemplateDAL,
pkiCollectionDAL, pkiCollectionDAL,
pkiAlertDAL, pkiAlertDAL,
pkiSubscriberDAL,
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateDAL, sshCertificateDAL,
@@ -653,7 +658,8 @@ export const projectServiceFactory = ({
autoCapitalization: update.autoCapitalization, autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization, enforceCapitalization: update.autoCapitalization,
hasDeleteProtection: update.hasDeleteProtection, hasDeleteProtection: update.hasDeleteProtection,
slug: update.slug slug: update.slug,
secretSharing: update.secretSharing
}); });
return updatedProject; return updatedProject;
@@ -1057,6 +1063,45 @@ export const projectServiceFactory = ({
}; };
}; };
/**
* Return list of PKI subscribers for project
*/
const listProjectPkiSubscribers = async ({
actorId,
actorOrgId,
actorAuthMethod,
actor,
projectId
}: TListProjectPkiSubscribersDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
const allowedSubscribers = [];
// (dangtony98): room to optimize
const subscribers = await pkiSubscriberDAL.find({ projectId });
for (const subscriber of subscribers) {
const canRead = permission.can(
ProjectPermissionPkiSubscriberActions.Read,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
if (canRead) {
allowedSubscribers.push(subscriber);
}
}
return allowedSubscribers;
};
/** /**
* Return list of certificate templates for project * Return list of certificate templates for project
*/ */
@@ -1156,17 +1201,15 @@ export const projectServiceFactory = ({
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId); const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId);
for (const host of hosts) { for (const host of hosts) {
try { const canRead = permission.can(
ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Read, subject(ProjectPermissionSub.SshHosts, {
subject(ProjectPermissionSub.SshHosts, { hostname: host.hostname
hostname: host.hostname })
}) );
);
if (canRead) {
allowedHosts.push(host); allowedHosts.push(host);
} catch {
// intentionally ignore projects where user lacks access
} }
} }
@@ -1930,6 +1973,7 @@ export const projectServiceFactory = ({
listProjectSshCas, listProjectSshCas,
listProjectSshHosts, listProjectSshHosts,
listProjectSshHostGroups, listProjectSshHostGroups,
listProjectPkiSubscribers,
listProjectSshCertificates, listProjectSshCertificates,
listProjectSshCertificateTemplates, listProjectSshCertificateTemplates,
updateVersionLimit, updateVersionLimit,

View File

@@ -93,6 +93,7 @@ export type TUpdateProjectDTO = {
autoCapitalization?: boolean; autoCapitalization?: boolean;
hasDeleteProtection?: boolean; hasDeleteProtection?: boolean;
slug?: string; slug?: string;
secretSharing?: boolean;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -155,6 +156,7 @@ export type TListProjectCertificateTemplatesDTO = TProjectPermission;
export type TListProjectSshCasDTO = TProjectPermission; export type TListProjectSshCasDTO = TProjectPermission;
export type TListProjectSshHostsDTO = TProjectPermission; export type TListProjectSshHostsDTO = TProjectPermission;
export type TListProjectSshCertificateTemplatesDTO = TProjectPermission; export type TListProjectSshCertificateTemplatesDTO = TProjectPermission;
export type TListProjectPkiSubscribersDTO = TProjectPermission;
export type TListProjectSshCertificatesDTO = { export type TListProjectSshCertificatesDTO = {
offset: number; offset: number;
limit: number; limit: number;

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