1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 19:39:33 +00:00

Compare commits

..

494 Commits

Author SHA1 Message Date
5bb0b7a508 K8s HA reference docs
A complete guide to k8s HA reference docs
2025-01-28 02:53:02 -05:00
1d5b629d8f Merge pull request from akhilmhdh/feat/region-flag
Added region flag for eu in cli
2025-01-22 13:21:14 -05:00
14f895cae2 Merge pull request from Infisical/readme-ssh
Add Infisical SSH to README
2025-01-22 12:56:21 -05:00
=
b7be6bd1d9 feat: removed region flag in description 2025-01-22 14:41:24 +05:30
58a97852f6 Merge pull request from akhilmhdh/fix/pro-trail-btn
fix: resolved pro trial button issue in sidebar
2025-01-22 01:58:32 -05:00
=
980aa9eaae fix: resolved pro trial button issue in sidebar 2025-01-22 12:25:54 +05:30
=
a35d1aa72b feat: removed root flag and added description for domain 2025-01-22 12:18:58 +05:30
52d801bce5 Add Infisical SSH to README 2025-01-21 22:36:55 -08:00
71ca7a82db Merge pull request from Infisical/vercel-project-help-text
Improvement: Vercel Integration Project Permission Helper Text
2025-01-21 21:43:53 -08:00
6cdc71b9b1 Merge pull request from Infisical/fix-org-sidebar-check
Fix: Correct Display Org Sidebar Check
2025-01-22 03:02:43 +01:00
f88d6a183f fix: correct display org sidebar check 2025-01-21 17:46:14 -08:00
fa82d4953e improvement: adjust casing 2025-01-21 16:05:11 -08:00
12d9fe9ffd improvement: add helper text to point vercel users to access permissions if they dont see their project listed 2025-01-21 16:02:24 -08:00
86acf88a13 Merge pull request from akhilmhdh/fix/project-delete
fix: resolved org sidebar not showing in org member detail window
2025-01-21 15:14:12 -05:00
=
63c7c39e21 fix: resolved org sidebar not showing in member detail window and other detail window 2025-01-22 01:39:44 +05:30
151edc7efa Merge pull request from Infisical/remove-enterprise-identity-limit
Remove Enterprise Member and Identity Limits for SAML/LDAP Login
2025-01-21 13:17:16 -05:00
5fa7f56285 Remove member and identity limits for enterprise saml and ldap login case 2025-01-21 10:12:23 -08:00
810b27d121 Merge pull request from Infisical/audit-log-stream-setup-error-improvement
Improvement: Propagate Upstream Error on Audit Log Stream Setup Fail
2025-01-21 10:06:23 -08:00
51fe7450ae improvement: propogate upstream error on audit log stream setup 2025-01-21 09:54:33 -08:00
938c06a2ed Merge pull request from Infisical/misc/added-more-scoping-for-namespace
misc: added more scoping logic for namespace installation
2025-01-21 11:09:45 -05:00
38d431ec77 misc: added more scoping logic for namespace installation 2025-01-21 23:57:11 +08:00
d202fdf5c8 Merge pull request from akhilmhdh/fix/project-delete
fix: resolved failing project delete
2025-01-21 10:42:59 -05:00
=
f1b2028542 fix: resolved failing project delete 2025-01-21 20:55:19 +05:30
5c9b46dfba Merge pull request from Infisical/misc/address-scim-email-verification-patch-issue
misc: address scim email verification patch issue
2025-01-21 20:23:08 +08:00
a516e50984 misc: address scim email verification patch issue 2025-01-21 17:01:34 +08:00
569439f208 Merge pull request from Infisical/misc/added-handling-for-case-enforcement
misc: added handling of secret case enforcement
2025-01-21 13:58:48 +08:00
9afc282679 Update deployment-pipeline.yml 2025-01-21 00:49:31 -05:00
8db85cfb84 Add slack alert 2025-01-21 00:04:14 -05:00
664b2f0089 add concurrency to git workflow 2025-01-20 23:52:47 -05:00
5e9bd3a7c6 Merge pull request from Infisical/expanded-secret-overview-adjustment
Fix: Adjust Secret Overview Expanded Secret View
2025-01-20 21:39:39 -05:00
2c13af6db3 correct helm verion 2025-01-20 21:37:13 -05:00
ec9171d0bc fix: adjust secret overview expanded secret view to accomodate nav restructure 2025-01-20 18:30:54 -08:00
81362bec8f Merge pull request from Infisical/daniel/cli-fix-2
fix: saml redirect failing due to blocked pop-ups
2025-01-20 13:21:32 -08:00
5a4d7541a2 Update SelectOrgPage.tsx 2025-01-20 22:13:52 +01:00
3c97c45455 others => other 2025-01-20 15:26:15 -05:00
4f015d77fb Merge pull request from akhilmhdh/feat/nav-restructure
Navigation restructure
2025-01-20 15:07:39 -05:00
=
78e894c2bb feat: changed user icon 2025-01-21 01:34:21 +05:30
=
23513158ed feat: updated nav ui based on feedback 2025-01-20 22:59:00 +05:30
=
934ef8ab27 feat: resolved plan text generator 2025-01-20 22:59:00 +05:30
=
23e9c52f67 feat: added missing breadcrumbs for integration pages 2025-01-20 22:59:00 +05:30
=
e276752e7c feat: removed trailing comma from ui 2025-01-20 22:59:00 +05:30
=
01ae19fa2b feat: completed all nav restructing and breadcrumbs cleanup 2025-01-20 22:58:59 +05:30
=
9df8cf60ef feat: finished breadcrumbs for all project types except secret manager 2025-01-20 22:58:59 +05:30
=
1b1fe2a700 feat: completed org breadcrumbs 2025-01-20 22:58:59 +05:30
=
338961480c feat: resolved accessibility issue with menu 2025-01-20 22:58:59 +05:30
=
03debcab5a feat: completed sidebar changes 2025-01-20 22:58:59 +05:30
4a6f759900 Merge pull request from akhilmhdh/feat/base64-decode-in-operator
Base64 decode in operator
2025-01-19 13:50:56 -05:00
b9d06ff686 update operator version 2025-01-19 13:50:24 -05:00
=
5cc5a4f03d feat: updated doc for the function name 2025-01-20 00:08:22 +05:30
=
5ef2be1a9c feat: updated function name 2025-01-20 00:08:06 +05:30
8de9ddfb8b update k8s operator doc 2025-01-19 11:29:55 -05:00
5b40de16cf improve docs and add missing function to create mamaged secret 2025-01-18 17:11:52 -05:00
=
11aac3f5dc docs: updated k8s operator template section with base64DecodeBytes content 2025-01-18 18:58:26 +05:30
=
9823c7d1aa feat: added base64DecodeBytes function to operator template 2025-01-18 18:58:00 +05:30
=
d627ecf05d feat: added region flag for eu in cli 2025-01-18 17:05:29 +05:30
3ba396f7fa Merge pull request from Infisical/parameter-store-error-message-improvement
Improvement: Add Secret Key to AWS Parameter Store Integreation Sync Error
2025-01-17 11:56:31 -08:00
9c561266ed improvement: add secret key to aws parameter store integreation error message 2025-01-17 08:51:41 -08:00
36fef11d91 Merge pull request from Infisical/misc/added-misisng-install-crd-flag-for-multi-installation
misc: added missing install CRD flag for multi-installation
2025-01-17 21:42:09 +08:00
742932c4a0 Merge pull request from Infisical/misc/add-org-id-to-org-settings
misc: add org id display to org settings
2025-01-17 17:31:43 +05:30
57a77ae5f1 misc: incremented chart and operator versions 2025-01-17 19:16:48 +08:00
7c9564c7dc misc: added missing install CRD flag for multi installation 2025-01-17 19:09:12 +08:00
736aecebf8 Merge pull request from Infisical/fix/address-org-invite-resend
fix: address org invite resend issues
2025-01-17 18:53:32 +08:00
16748357d7 misc: addressed comments 2025-01-17 03:50:01 +08:00
12863b389b Merge pull request from Infisical/daniel/unique-group-names
feat(groups): unique names
2025-01-16 20:41:07 +01:00
c592ff00a6 fix: address org invite resend 2025-01-17 01:42:35 +08:00
ef87086272 fix(groups): unique names, requested changes 2025-01-16 17:38:54 +01:00
bd459d994c misc: finalized error message 2025-01-16 19:55:21 +08:00
440f93f392 misc: added handling for case enforcement 2025-01-16 19:10:00 +08:00
b440e918ac Merge pull request from Infisical/daniel/audit-logs-searchability
feat(radar): pagination and filtering
2025-01-16 01:24:24 +01:00
439f253350 Merge pull request from Infisical/daniel/secret-scans-export
feat(radar): export radar data
2025-01-16 01:22:05 +01:00
4e68304262 fix: type check 2025-01-16 01:08:43 +01:00
c4d0896609 feat(groups): unique names 2025-01-16 01:01:01 +01:00
b8115d481c Merge pull request from Infisical/daniel/audit-log-docs
docs(audit-logs): audit log streams structure
2025-01-15 18:07:24 +01:00
5fdec97319 requested changes 2025-01-15 17:50:30 +01:00
755bb1679a requested changes 2025-01-15 17:32:53 +01:00
7142e7a6c6 Update secret-scanning-dal.ts 2025-01-15 16:39:08 +01:00
11ade92b5b Update secret-scanning-service.ts 2025-01-15 16:39:08 +01:00
4147725260 feat(radar): pagination & filtering 2025-01-15 16:39:08 +01:00
c359cf162f requested changes 2025-01-15 16:37:42 +01:00
9d04b648fa misc: add org id display to org settings 2025-01-15 20:21:10 +08:00
9edfdb7234 docs(audit-logs): audit log streams structure 2025-01-15 02:33:21 +01:00
ff74e020fc requested changes 2025-01-15 01:55:10 +01:00
6ee446e574 Merge pull request from akhilmhdh/refactor/permission-service-fn
Refactored permission service for project to have project operation type validation
2025-01-14 23:50:10 +05:30
c806059b11 Merge pull request from Infisical/misc/add-oidc-saml-handling-for-login-check
misc: add oidc saml handling for login SA check
2025-01-14 13:11:29 -05:00
=
3a5bb31bde feat: updated all permission argument to actionProjectType 2025-01-14 23:41:06 +05:30
=
6f38d6c76f feat: updated to enum ActionProjectType 2025-01-14 23:26:18 +05:30
=
d721a46ec9 fix: resolved secret and approval count triggered for other project types 2025-01-14 23:26:18 +05:30
=
989065ba34 refactor: updated permission service for projects to check for operation type as well 2025-01-14 23:26:18 +05:30
5ad419c079 misc: updated comment 2025-01-15 00:49:53 +08:00
80f72e8040 misc: added context 2025-01-15 00:48:19 +08:00
0cef728617 Merge pull request from Infisical/misc/addressed-cf-pages-error-304
fix: addressed cloudflare pages error 304
2025-01-14 10:42:42 -05:00
0a735422ed misc: standardized log 2025-01-14 23:39:47 +08:00
8370a0d9c0 misc: added log 2025-01-14 23:29:04 +08:00
71af662998 fix: addressed cloudflare pages error 304 2025-01-14 23:18:38 +08:00
27e391f7e0 misc: add oidc saml handling for login check 2025-01-14 21:02:41 +08:00
2187c7588c update cache control to be no store 2025-01-13 23:58:36 -05:00
8ab7e8360d add cache busting param 2025-01-13 22:53:04 -05:00
957cab3117 Merge pull request from Infisical/fix-copy-user-id
Fix: Copy Username on User Details Page
2025-01-13 20:06:11 -05:00
4bf16f68fc fix: correctly copy user email to clipboard 2025-01-13 17:03:08 -08:00
ab3ee775bb feat(radar): export radar data 2025-01-14 00:53:40 +01:00
2a86e6f4d1 add cache contro to cloudflare pages integration 2025-01-13 17:17:39 -05:00
194fbb79f2 Merge pull request from Infisical/daniel/custom-cors
feat(api): custom cors settings
2025-01-13 14:36:19 -05:00
faaba8deb7 add metabase link to on call guide 2025-01-13 11:16:49 -05:00
ae21b157a9 Merge pull request from Infisical/misc/added-proper-error-propagation-for-aws-kms
misc: added error propagation for aws kms
2025-01-13 23:46:57 +08:00
6167c70a74 Merge pull request from Infisical/daniel/push-secret-docs
docs: small k8s docs improvements
2025-01-13 10:39:45 -05:00
0ecf75cbdb misc: scoped down to AWS-related errors 2025-01-13 23:18:24 +08:00
3f8aa0fa4b misc: added error propagation for aws kms 2025-01-13 23:00:40 +08:00
6487c83bda docs: requested changes 2025-01-13 14:35:36 +01:00
c08fbbdab2 Update app.ts 2025-01-13 13:59:25 +01:00
a4aa65bb81 add on call to hand book 2025-01-13 01:27:50 -05:00
258d19cbe4 Merge pull request from Infisical/daniel/shared-secret-audit-logs
feat(audit-logs): shared secrets audit logs
2025-01-10 22:29:05 +01:00
de91356127 Update envars.mdx 2025-01-10 22:24:57 +01:00
ccb07942de feat(api): custom cors settings 2025-01-10 22:23:19 +01:00
3d278b0925 feat(audit-logs): shared secrets audit logs 2025-01-10 21:37:10 +01:00
956fb2efb4 docs: better k8s pre-req 2025-01-10 20:16:30 +01:00
13894261ce docs: small k8s docs improvements 2025-01-10 20:10:00 +01:00
d7ffa70906 Merge pull request from Infisical/misc/suppot-array-value-for-oidc-aud-field
misc: add support for array values in OIDC aud field
2025-01-10 13:32:09 -05:00
b8fa7c5bb6 Merge pull request from Infisical/misc/fix-shared-secret-within-org-and-login-redirect
misc: resolve shared secret within org and added login redirect
2025-01-11 02:16:48 +08:00
2baacfcd8f misc: add support for array values 2025-01-11 02:07:04 +08:00
31c11f7d2a Merge pull request from Infisical/vmatsiiako-typo-patch-1
Update infisical-dynamic-secret-crd.mdx
2025-01-10 12:43:40 -05:00
c5f06dece4 Merge pull request from Infisical/misc/made-is-active-optional-for-integration-patch
misc: made is active optional for integration patch
2025-01-11 00:26:21 +08:00
662e79ac98 Merge pull request from Infisical/daniel/audit-log-timestamp-bug
fix(audit-logs): time conversion bug
2025-01-10 17:23:58 +01:00
17249d603b fix: add delete secrets event type to frontend 2025-01-10 17:15:26 +01:00
9bdff9c504 fix: explicit postgres time conversion 2025-01-10 17:15:14 +01:00
4552ce6ca4 Merge pull request from Infisical/misc/add-secret-key-indicator-for-failed-integrations-when-possible
misc: add secret key indicator for failed AWS integration syncs
2025-01-10 11:10:25 -05:00
ba4b8801eb Merge pull request from akhilmhdh/fix/broken-secret-creation
Resolve self signed error for mssql
2025-01-10 11:01:54 -05:00
36a5f728a1 misc: add secret key indicator for failed AWS integration syncs 2025-01-11 00:00:17 +08:00
502429d914 misc: resolve shared secret within org and added login redirect 2025-01-10 23:55:21 +08:00
27abfa4fff Merge pull request from Infisical/misc/add-pagination-handling-for-gitlab-groups-fetch
misc: add pagination handling for gitlab groups fetch
2025-01-10 19:15:38 +08:00
4bc9bca287 removed undefined type 2025-01-10 19:08:49 +08:00
612c29225d misc: add pagination handling for gitlab groups fetch 2025-01-10 19:06:10 +08:00
=
4d43accc8a fix: did same resolution for dynamic secret ops as well 2025-01-10 15:12:04 +05:30
3c89a69410 Update infisical-dynamic-secret-crd.mdx 2025-01-10 01:39:12 -08:00
=
e741b63e63 fix: resolved mssql self signed error 2025-01-10 14:59:49 +05:30
9cde1995c7 Merge pull request from Infisical/fix/address-indefinite-hang-for-run-watch-failure
fix: address infinite hang when infisical run watch fails
2025-01-10 11:54:51 +08:00
3ed3856c85 Merge pull request from akhilmhdh/fix/broken-secret-creation
feat: added validation for secret name to disallow spaces and overview page
2025-01-09 15:38:25 -05:00
=
8054d93851 feat: added validation for secret name to disallow spaces and overview page fix for folder creation 2025-01-10 02:00:39 +05:30
02dc23425c fix: address infinite hang when infisical run watch fails 2025-01-10 02:29:39 +08:00
01534c3525 Merge pull request from Infisical/daniel/k8s-dynamic-secrets
feat(k8-operator): dynamic secrets
2025-01-09 12:45:30 -05:00
325ce73b9f fix typo 2025-01-09 12:36:52 -05:00
1639bda3f6 remove copy-paste and make redeploy docs specific 2025-01-09 12:23:46 -05:00
b5b91c929f fix kubernetes docs 2025-01-09 12:11:48 -05:00
9bc549ca8c Merge pull request from Infisical/feat/add-initial-sync-behavior-for-gitlab
feat: add initial sync behavior for gitlab
2025-01-10 01:01:17 +08:00
cedc88d83a misc: removed comment 2025-01-10 00:42:16 +08:00
f866a810c1 Merge pull request from Infisical/feat/secret-access-list
feat: secret access list
2025-01-10 00:40:56 +08:00
0c034d69ac misc: removed raw from api 2025-01-10 00:32:31 +08:00
e29f7f656c docs(k8s): better documentation layout 2025-01-09 16:07:07 +01:00
858e569d4d feat: add initial sync behavior for gitlab 2025-01-09 20:43:14 +08:00
5d8f32b774 Merge pull request from thomas-infisical/patch-1
Doc: Update Nov & Dec Changelogs
2025-01-08 21:42:49 -08:00
bb71f5eb7e Merge pull request from Infisical/daniel/update-k8s-manifest
fix: k8's manifest out of sync
2025-01-08 17:25:21 -05:00
30b431a255 Merge pull request from Infisical/bump-ecs-deploy-task-definition
ci: Bump aws-actions/amazon-ecs-deploy-task-definition to v2
2025-01-08 16:22:35 -05:00
fd32118685 Bump aws-actions/amazon-ecs-deploy-task-definition to v2
Bump aws-actions/amazon-ecs-deploy-task-definition to resolve:

```
Error: Failed to register task definition in ECS: Unexpected key 'enableFaultInjection' found in params
Error: Unexpected key 'enableFaultInjection' found in params
```

errors.
2025-01-08 13:20:41 -08:00
8528f3fd2a Merge pull request from Infisical/feat/resource-metadata
feat: resource metadata
2025-01-08 14:47:01 -05:00
60749cfc43 misc: made is active optional for integration patch 2025-01-09 02:19:24 +08:00
eb358bcafd Update install-secrets-operator.yaml 2025-01-08 16:26:50 +01:00
ffbd29c575 doc: update nov & dec changelog
First try at updating the Changelog from these past months.

Let me know if you think of anything I might have forgotten.
2025-01-08 15:50:53 +01:00
a74f0170da Merge pull request from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: resolved conditions permission not getting added from new policy…
2025-01-08 22:26:45 +08:00
=
a0fad34a6d fix: resolved conditions permission not getting added from new policy button 2025-01-08 19:48:13 +05:30
f0dc5ec876 misc: add secret access insights to license fns 2025-01-08 20:56:33 +08:00
c2453f0c84 misc: finalized ui 2025-01-08 20:50:20 +08:00
2819c8519e misc: added license checks 2025-01-08 20:31:04 +08:00
616b013b12 Merge remote-tracking branch 'origin/main' into feat/secret-access-list 2025-01-08 20:08:16 +08:00
0b9d890a51 Merge pull request from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: address sso redirect and project role creation
2025-01-08 19:20:19 +08:00
5ba507bc1c misc: made installation ID optional for github 2025-01-08 19:17:38 +08:00
=
0ecc196e5d feat: added missing coerce in azure key vault 2025-01-08 16:30:46 +05:30
=
ddac9f7cc4 feat: made all redirect route to coerce it 2025-01-08 16:27:56 +05:30
f5adc4d9f3 misc: removed unnecessary type assertion 2025-01-08 18:44:58 +08:00
34354994d8 fix: address sso redirect and project role creation 2025-01-08 18:29:25 +08:00
d7c3192099 feat: frontend integration 2025-01-08 18:24:40 +08:00
1576358805 Merge pull request from Infisical/auth0-saml
Add Support for Auth0 SAML
2025-01-07 15:04:44 -05:00
e6103d2d3f docs: update initial saml setup steps 2025-01-07 11:45:07 -08:00
8bf8bc77c9 Merge pull request from akhilmhdh/fix/broken-image
feat: resolved app not loading on org no access
2025-01-07 14:29:47 -05:00
=
3219723149 feat: resolved app not loading on org no access 2025-01-08 00:55:22 +05:30
74b95d92ab feat: backend setup 2025-01-08 02:48:47 +08:00
6d3793beff Merge pull request from akhilmhdh/fix/broken-image
Fixed broke image
2025-01-08 00:18:38 +05:30
0df41f3391 Merge pull request from Infisical/fix-user-groups-plan
Improvement: Clarify Enterprise Plan for User Group Feature Upgrade Modal
2025-01-07 10:44:32 -08:00
=
1acac9d479 feat: resolved broken gitlab image and hidden the standalone endpoints from api documentation 2025-01-08 00:14:31 +05:30
0cefd6f837 Run linter 2025-01-08 01:41:22 +07:00
5e9dc0b98d clarify enterprise plan for user group feature upgrade modal 2025-01-07 10:38:09 -08:00
f632847dc6 Merge branch 'main', remote-tracking branch 'origin' into auth0-saml 2025-01-08 01:35:11 +07:00
faa6d1cf40 Add support for Auth0 SAML 2025-01-08 01:34:03 +07:00
7fb18870e3 Merge pull request from akhilmhdh/feat/updated-saml-error-message
Updated saml error message
2025-01-07 10:57:03 -05:00
ae841715e5 update saml error message 2025-01-07 10:56:44 -05:00
=
baac87c16a feat: updated saml error message on missing email or first name attribute 2025-01-07 21:17:25 +05:30
291d29ec41 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2025-01-07 19:15:33 +08:00
b726187ba3 Merge pull request from mr-ssd/patch-1
docs: add note for dynamic secrets as paid feature
2025-01-07 14:12:50 +05:30
d98ff32b07 Merge pull request from mr-ssd/patch-2
docs: add note Approval Workflows as paid feature
2025-01-07 14:12:12 +05:30
1fa510b32f Merge pull request from Infisical/feat/target-specific-azure-key-vault-tenant
feat: target specific azure key vault tenant
2025-01-07 14:05:14 +08:00
c57f0d8120 Merge pull request from Infisical/misc/made-secret-path-input-show-correct-folder
misc: made secret path input show correct folders
2025-01-06 23:28:36 -05:00
00490f2cff misc: made secret path input show correct folders based on env in integrations 2025-01-07 12:08:09 +08:00
ee58f538c0 feat: target specific azure key vault tenant 2025-01-07 11:53:17 +08:00
0fa20f7839 Merge pull request from Infisical/aws-sm-integration-force-delete
Fix: Set Force Flag on Delete Secret Command for AWS Secrets Manager
2025-01-06 20:51:48 -05:00
40ef75d3bd fix: use force flag when deleting secrets from aws secret manager integration 2025-01-06 16:11:32 -08:00
26af13453c Merge pull request from akhilmhdh/fix/text-typo
fix: resolved typo in layout
2025-01-06 17:31:06 -05:00
=
ad1f71883d fix: resolved typo in layout 2025-01-07 02:58:56 +05:30
aa39451bc2 fix: generated files 2025-01-06 22:03:56 +01:00
f5548b3e8c Merge branch 'heads/main' into daniel/k8s-dynamic-secrets 2025-01-06 22:00:21 +01:00
2659ea7170 Merge pull request from Infisical/misc/updated-openssh-installation-for-fips
misc: updated openssh installation for fips
2025-01-06 14:08:07 -05:00
d2e3f504fd misc: updated openssh installation for fips 2025-01-07 03:04:57 +08:00
ca4151a34d Merge pull request from akhilmhdh/refactor/rbr
Adios nextjs
2025-01-06 14:01:22 -05:00
=
d4bc104fd1 feat: adios nextjs 2025-01-06 18:38:57 +00:00
=
7e3a3fcdb0 feat: updated the installation id to coerce string 2025-01-06 23:37:16 +05:30
=
7f67912d2f feat: resolved failing be lint check 2025-01-06 22:18:49 +05:30
=
a7f4020c08 feat: bumped nodejs from 16 to 20 2025-01-06 22:13:56 +05:30
=
d2d89034ba feat: moved more of isLoading to isPending 2025-01-06 22:13:16 +05:30
=
2fc6c564c0 feat: removed all existBeforeEnter error 2025-01-06 19:56:53 +05:30
=
240b86c1d5 fix: resolved project list issue and app connection github not listed 2025-01-06 19:48:50 +05:30
=
30d66cef89 feat: resolved slack failing and approval url correction 2025-01-06 19:18:10 +05:30
=
b7d11444a9 feat: resolved bug on org change select and project on change 2025-01-06 19:01:10 +05:30
=
0a6aef0afa feat: lint fixes 2025-01-06 14:56:51 +05:30
=
0ac7ec460b feat: resolving some bugs 2025-01-06 14:56:50 +05:30
=
808a901aee feat: updated env in docker files 2025-01-06 14:56:50 +05:30
=
d5412f916f feat: updated frontend use run time config inject value 2025-01-06 14:56:50 +05:30
=
d0648ca596 feat: added runtime env config endpoint 2025-01-06 14:56:50 +05:30
=
3291f1f908 feat: resolved typo in integration redirects 2025-01-06 14:56:50 +05:30
=
965d30bd03 feat: updated docker 2025-01-06 14:56:50 +05:30
=
68fe7c589a feat: updated backend to support bare react as standalone 2025-01-06 14:56:50 +05:30
=
54377a44d3 feat: renamed old frontend and fixed some minor bugs 2025-01-06 14:56:49 +05:30
=
8c902d7699 feat: added error page and not found page handler 2025-01-06 14:55:14 +05:30
=
c25c84aeb3 feat: added csp and minor changes 2025-01-06 14:55:14 +05:30
=
4359eb7313 feat: testing all todo comments and cli 2025-01-06 14:55:13 +05:30
=
322536d738 feat: completed migration of all integrations pages 2025-01-06 14:55:13 +05:30
=
6c5db3a187 feat: completed secret manager integration list and detail page 2025-01-06 14:55:13 +05:30
=
a337e6badd feat: bug fixes in overview page and dashboard page 2025-01-06 14:55:13 +05:30
=
524a97e9a6 fix: resolved infinite rendering issue caused by zustand 2025-01-06 14:55:13 +05:30
=
c56f598115 feat: switched to official lottie react and adjusted all the existings ones 2025-01-06 14:55:13 +05:30
=
19d32a1a3b feat: completed migration of ssh product 2025-01-06 14:55:12 +05:30
=
7e5417a0eb feat: completed migration of app connection 2025-01-06 14:55:12 +05:30
=
afd6de27fe feat: re-arranged route paths 2025-01-06 14:55:12 +05:30
=
7781a6b7e7 feat: added static images and fixing minor layout issues 2025-01-06 14:55:12 +05:30
=
b3b4e41d92 feat: resolved ico header, failing translation and lottie icon issues 2025-01-06 14:55:12 +05:30
=
5225f5136a feat: resolving issues on loader 2025-01-06 14:55:12 +05:30
=
398adfaf76 feat: resolved all ts issues 2025-01-06 14:55:12 +05:30
=
d77c26fa38 feat: resolved almost all ts issues 2025-01-06 14:55:11 +05:30
=
ef7b81734a feat: added kms routes 2025-01-06 14:55:11 +05:30
=
09b489a348 feat: completed cert routes 2025-01-06 14:55:11 +05:30
=
6b5c50def0 feat: added secret-manager routes 2025-01-06 14:55:11 +05:30
=
1f2d52176c feat: added org route 2025-01-06 14:55:11 +05:30
=
7002e297c8 feat: removed routes and added kms, cert to pages setup 2025-01-06 14:55:11 +05:30
=
71864a131f feat: changed to virtual route for secret-manager 2025-01-06 14:55:11 +05:30
=
9964d2ecaa feat: completed switch to virtual for org pages 2025-01-06 14:55:10 +05:30
=
3ebbaefc2a feat: changed to virtual route for auth and personal settings 2025-01-06 14:55:10 +05:30
=
dd5c494bdb feat: secret manager routes migration completed 2025-01-06 14:55:10 +05:30
=
bace8af5a1 feat: removed $organizationId from the routes 2025-01-06 14:55:10 +05:30
=
f56196b820 feat: completed project layout base 2025-01-06 14:55:10 +05:30
=
7042d73410 feat: upgrade react-query to v5 and changed hooks to staletime inf for prev context based ones 2025-01-06 14:55:09 +05:30
=
cb22ee315e feat: resolved a lot of ts issues, planning to migrate to v5 tanstack query 2025-01-06 14:55:09 +05:30
=
701eb7cfc6 feat: resolved breaking dev 2025-01-06 14:55:09 +05:30
=
bf8df14b01 feat: added hoc, resolved dropdown type issue 2025-01-06 14:55:09 +05:30
=
1ba8b6394b feat: added virtual route to mount the layout without wrapping again 2025-01-06 14:55:09 +05:30
=
c442c8483a feat: completed signup and personal settings ui 2025-01-06 14:55:09 +05:30
=
0435305a68 feat: resolved eslint issues and seperated org layout to make it simple 2025-01-06 14:55:09 +05:30
=
febf11f502 feat: added organization layout base with subscription and user loading 2025-01-06 14:55:08 +05:30
=
64fd15c32d feat: modified backend token endpoint to return organizationid 2025-01-06 14:55:08 +05:30
=
a2c9494d52 feat: added signup routes and moved server config to router context 2025-01-06 14:55:08 +05:30
=
18460e0678 feat: base for signup pages completed 2025-01-06 14:55:08 +05:30
=
3d03fece74 feat: first login page base completed 2025-01-06 14:55:08 +05:30
=
234e7eb9be feat: added ui components 2025-01-06 14:55:08 +05:30
=
04af313bf0 feat: added all old dependencies 2025-01-06 14:55:08 +05:30
=
9b038ccc45 feat: added tanstack router 2025-01-06 14:55:07 +05:30
=
9beb384546 feat: added tailwindcss with prettier tailwind support 2025-01-06 14:55:07 +05:30
=
12ec9b4b4e feat: added type check, lint and prettier 2025-01-06 14:55:07 +05:30
96b8e7fda8 Merge pull request from Infisical/vmatsiiako-wiki-patch-1 2025-01-01 18:12:48 -05:00
93b9108aa3 Update onboarding.mdx 2025-01-01 15:04:50 -08:00
99017ea1ae Merge pull request from Infisical/akhilmhdh-patch-azure
fix: resolved azure app config api not pulling all keys
2024-12-30 23:19:54 +08:00
f32588112e fix: resolved azure app config api not pulling all keys 2024-12-29 19:51:59 +05:30
f9b0b6700a Merge pull request from Infisical/feat/authenticate-key-vault-against-specific-tenant
feat: auth key vault against specific tenant
2024-12-29 08:18:52 -05:00
b45d9398f0 Merge pull request from Infisical/vmatsiiako-intercom-patch-1 2024-12-27 21:47:36 -05:00
1d1140237f Update azure-app-configuration.mdx 2024-12-27 00:59:49 -05:00
937560fd8d Update azure-app-configuration.mdx 2024-12-27 00:58:48 -05:00
5f4b7b9ea7 Merge pull request from Infisical/patch-azure-label
Azure App Config Label patch
2024-12-27 00:40:42 -05:00
05139820a5 add docs for label and refereces 2024-12-26 16:36:27 -05:00
7f6bc3ecfe misc: added additional note for azure app config 2024-12-26 19:07:45 +08:00
d8cc000ad1 feat: authenticate key vault against specific tenant 2024-12-26 19:07:16 +08:00
8fc03c06d9 handle empty label 2024-12-26 01:17:47 -05:00
50ceedf39f Remove intercom from docs 2024-12-25 16:40:45 -08:00
550096e72b Merge pull request from Mhammad-riyaz/riyaz/query
Fix Error When Clicking on CA Table Row After Viewing Certificate in Certificate Authorities Tab
2024-12-25 02:05:27 -08:00
1190ca2d77 docs: add note Approval Workflows as paid feature 2024-12-25 15:27:38 +07:00
2fb60201bc add not for dynamic secrets as paid feature 2024-12-25 14:17:27 +07:00
e763a6f683 misc: added default for metadataSyncMode 2024-12-23 21:29:51 +08:00
cb1b006118 misc: add secret metadata to batch update 2024-12-23 14:46:23 +08:00
356e7c5958 feat: added approval handling for metadata in replicate 2024-12-23 14:36:41 +08:00
634b500244 Merge pull request from Infisical/temp-hide-app-connection-docs 2024-12-20 18:53:20 -05:00
54b4d4ae55 docs: temp hide app connections 2024-12-20 15:07:23 -08:00
1a68765f15 feat: secret approval and replication 2024-12-21 02:51:47 +08:00
2f6dab3f63 Merge pull request from Infisical/ssh-cli-docs
Documentation for SSH Command in CLI
2024-12-20 08:49:38 -08:00
ae07d38c19 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2024-12-20 14:23:14 +08:00
e9564f5231 Fix ssh cli docs based on review 2024-12-19 22:12:32 -08:00
05cdca9202 Add docs for SSH CLI 2024-12-19 19:47:24 -08:00
5ab0c66dee Merge pull request from Infisical/cli-ssh-agent
Add SSH CLI capability to load issued SSH credentials into SSH Agent via addToAgent flag
2024-12-19 11:58:02 -08:00
f5a0641671 Add requirement for ssh issue credentials command to include either outFilePath or addToAgent flag 2024-12-19 11:55:44 -08:00
2843818395 Merge pull request from Infisical/misc/add-custom-metrics-for-errors
misc: added metric for api errors
2024-12-19 14:15:13 -05:00
2357f3bc1f misc: added integration ID 2024-12-20 03:10:59 +08:00
cde813aafb misc: added custom metrics for integration syncs 2024-12-20 03:08:55 +08:00
bbc8091d44 misc: added metric for api errors 2024-12-20 02:37:44 +08:00
ce5e591457 Merge pull request from Infisical/daniel/vercel-integration-bug
fix(vercel-integration): vercel integration initial sync behavior
2024-12-19 19:05:31 +01:00
5ae74f9761 Update create.tsx 2024-12-19 18:53:17 +01:00
eef331bbd1 Merge pull request from Infisical/app-connections
Feat: App Connections
2024-12-19 09:51:38 -08:00
d5c2e9236a fix: doc typo 2024-12-19 09:43:14 -08:00
025b4b8761 feat: aws secret manager metadata sync 2024-12-19 23:54:47 +08:00
13eef7e524 Merge pull request from Infisical/role-description-schema-fix
Fix: Correct Role Description Schema to Accept Null
2024-12-19 07:09:52 -08:00
ef688efc8d feat: integrated secret metadata to ui 2024-12-19 22:02:20 +08:00
8c98565715 feat: completed basic CRUD 2024-12-19 17:47:39 +08:00
f97f98b2c3 Add ability to load retrieved ssh credentials into ssh agent with new addToAgent flag 2024-12-18 23:27:11 -08:00
e9358cd1d8 misc: backend setup + create secret metadata 2024-12-19 15:05:16 +08:00
3fa84c578c fix: correct role description schema to accept null 2024-12-18 22:15:40 -08:00
c22ed04733 fix: correct imports to use alias 2024-12-18 21:30:36 -08:00
64fac1979f revert: mint and license 2024-12-18 21:21:28 -08:00
2d60f389c2 improvements: address feedback 2024-12-18 21:18:38 -08:00
7798e5a2ad fix: default behavior 2024-12-19 01:25:29 +01:00
ed78227725 fix(vercel-integration): initial sync logic 2024-12-19 01:18:47 +01:00
89848a2f5c Merge pull request from Infisical/ssh-cli
CLI - SSH Capabilities
2024-12-18 14:12:06 -08:00
1936f7cc4f Merge pull request from Infisical/ssh-certs
Add OpenSSH dependency to standalone Dockerfiles
2024-12-18 11:55:46 -08:00
1adeb5a70d Add openssh dependency to standalone Dockerfiles 2024-12-18 11:51:13 -08:00
058475fc3f Ran go mod tidy 2024-12-18 11:45:09 -08:00
ee4eb7f84b Update Go SDK version dependency 2024-12-18 11:32:09 -08:00
8122433f5c Merge pull request from Infisical/ssh-certs
Expose ssh endpoints to api reference, update ssh sign/issue endpoints
2024-12-18 14:02:10 -05:00
a0411e3ba8 Expose ssh endpoints to api reference, update ssh sign/issue endpoints 2024-12-18 10:59:28 -08:00
62968c5e43 merge main 2024-12-18 10:09:23 -08:00
f3cf1a3f50 Merge pull request from Infisical/ssh-certs
Infisical SSH (SSH Certificates)
2024-12-18 12:40:24 -05:00
b4b417658f Update ssh cli api error messages 2024-12-18 09:22:06 -08:00
fed99a14a8 Merge remote-tracking branch 'origin' into ssh-certs 2024-12-18 09:20:13 -08:00
d4cfee99a6 Update ssh docs 2024-12-18 09:20:02 -08:00
e70ca57510 update minor version 2024-12-18 10:48:11 -05:00
06f321e4bf Update Chart.yaml 2024-12-18 10:44:51 -05:00
3c3fcd0db8 update k8 version to 7.7 2024-12-18 10:44:37 -05:00
21eb2bed7e Merge pull request from Infisical/daniel/k8-push-secret
feat(k8-operator): push secrets
2024-12-18 00:26:55 -05:00
31a21a432d Update isValidKeyAlgorithm impl 2024-12-17 20:40:03 -08:00
381960b0bd Add docs for Infisical SSH 2024-12-17 20:35:02 -08:00
7eb05afe2a misc: better condition naming 2024-12-18 04:09:44 +01:00
0b54948b15 fix: better condition error 2024-12-18 04:09:44 +01:00
39e598e408 fix: move fixes from different branch 2024-12-18 04:09:44 +01:00
b735618601 fix(k8-operator): resource-based finalizer names 2024-12-18 04:09:44 +01:00
3a5e862def fix(k8-operator): helm and cleanup 2024-12-18 04:09:44 +01:00
d1c4a9c75a updated env slugs 2024-12-18 03:28:32 +01:00
5532844ee7 Added RBAC 2024-12-18 03:28:32 +01:00
dd5aab973f Update PROJECT 2024-12-18 03:28:32 +01:00
ced12baf5d Update conditions.go 2024-12-18 03:28:32 +01:00
7db1e62654 fix: requested changes 2024-12-18 03:28:32 +01:00
0ab3ae442e cleanup and resource seperation 2024-12-18 03:28:32 +01:00
ed9472efc8 remove print 2024-12-18 03:28:32 +01:00
e094844601 docs(k8-operator): push secrets 2024-12-18 03:28:32 +01:00
e761b49964 feat(k8-operator): push secrets 2024-12-18 03:28:32 +01:00
6a8be75b79 Merge pull request from Infisical/daniel/fix-reminder-cleanup
fix(secret-reminders): proper cleanup on deleted resources
2024-12-18 02:57:07 +01:00
4daaf80caa fix: better naming 2024-12-18 02:56:13 +01:00
cf7768d8e5 Merge branch 'daniel/k8-push-secret' into daniel/k8s-dynamic-secrets 2024-12-18 01:41:17 +01:00
e76d2f58ea fix: move fixes from different branch 2024-12-18 01:39:32 +01:00
a92e61575d fix: test types 2024-12-18 01:10:23 +01:00
761007208d misc: daily cleanup of rogue secret reminder jobs 2024-12-17 23:32:45 +01:00
cc3e0d1922 fix: remove completed and failed reminder jobs 2024-12-17 23:32:04 +01:00
765280eef6 Update ssh cli issue/sign according to review 2024-12-17 13:12:47 -08:00
215761ca6b Merge pull request from Infisical/daniel/azure-label
feat(azure-app-integration): label & reference support
2024-12-17 20:56:53 +01:00
0977ff1e36 Enforce min length on certificate template id param for ssh issue/sign operations 2024-12-17 11:13:22 -08:00
c6081900a4 Merge pull request from Infisical/misc/consolidated-missing-helm-conditions-for-namespace-installation
misc: added missing helm configs for namespace installation
2024-12-17 14:11:21 -05:00
86800c0cdb Update ssh issue/sign fns to be based on certificate template id 2024-12-17 10:37:05 -08:00
1fa99e5585 Begin docs for ssh 2024-12-17 10:16:10 -08:00
7947e73569 fix: correct import path to use alias 2024-12-17 09:21:23 -08:00
8f5bb44ff4 Merge pull request from akhilmhdh/fix/broken-breakcrumb 2024-12-17 09:38:30 -05:00
3f70f08e8c doc: added rationale for namespace installation 2024-12-17 22:15:54 +08:00
078eaff164 Merge pull request from Infisical/misc/remove-encrypted-data-key-from-org-response
misc: remove encrypted data key from org response
2024-12-17 21:45:28 +08:00
221aa99374 Merge pull request from Pranav2612000/improv/2845-dont-close-modal-on-outside-click-while-adding-secret
improv ui: don't close "Create Secrets" modal when clicking outside it
2024-12-17 19:08:43 +05:30
6a681dcf6a improv ux: don't close 'Create secrets' modal when clicking outside it
Fixes 
2024-12-17 19:02:50 +05:30
b99b98b6a4 misc: remove encrypted data key from org response 2024-12-17 21:24:56 +08:00
d7271b9631 improv ui: use radix modal mode for Modals
Using the modal mode ensures that interaction with outside elements
is disabled ( for e.g scroll ) and only dialog content is visible to
screen readers.
2024-12-17 18:49:37 +05:30
379e526200 Merge pull request from Infisical/fix/false-org-error
fix: resolves a false org not logged in error
2024-12-17 16:53:09 +05:30
=
1f151a9b05 feat: resolved broken breakcrumb in secret manager 2024-12-17 16:49:05 +05:30
6b2eb9c6c9 fix: resolves a false org not logged in error 2024-12-17 14:37:41 +05:30
52ce90846a feature: app connections 2024-12-16 22:46:08 -08:00
be36827392 Finish ssh cli sign/issue commands 2024-12-16 17:42:11 -08:00
68a3291235 misc: requested changes 2024-12-16 23:24:08 +01:00
471f47d260 Fix ssh ca page backward redirect link 2024-12-16 12:26:37 -08:00
ccb757ec3e fix: missed transaction 2024-12-16 20:58:56 +01:00
b669b0a9f8 Merge pull request from Infisical/feat/sync-circle-ci-context
feat: circle ci context integration
2024-12-17 02:12:32 +08:00
9e768640cd misc: made scope project the default 2024-12-17 00:12:25 +08:00
35f7420447 misc: added missing helm configs 2024-12-16 23:54:43 +08:00
c6a0e36318 fix(api): secret reminders not getting deleted 2024-12-16 15:55:15 +01:00
181ba75f2a fix(dashboard): creation of new org when user is apart of no orgs 2024-12-16 15:55:14 +01:00
c00f6601bd fix(secrets-api): deletion of secret reminders on secret delete 2024-12-16 15:53:56 +01:00
111605a945 fix: ui improvement 2024-12-16 15:32:38 +01:00
2ac110f00e fix: requested changes 2024-12-16 15:32:38 +01:00
0366506213 feat(azure-app-integration): label & reference support 2024-12-16 15:32:38 +01:00
e3d29b637d misc: added type assertion 2024-12-16 22:27:29 +08:00
9cd0dc8970 Merge pull request from akhilmhdh/fix/group-access-failing 2024-12-16 09:25:01 -05:00
f8f5000bad misc: addressed review comments 2024-12-16 22:20:59 +08:00
40919ccf59 misc: finalized docs and other details 2024-12-16 20:15:14 +08:00
=
44303aca6a fix: group only access to project failing 2024-12-16 16:09:05 +05:30
4bd50c3548 misc: unified to a single integration 2024-12-16 16:08:51 +08:00
fb253d00eb Move ssh out to org level 2024-12-15 20:43:13 -08:00
097512c691 Begin adding ssh commands to cli 2024-12-15 17:30:57 -08:00
64a982d5e0 Merge pull request from akhilmhdh/feat/split-project
feat: changed multi insert into batch insert
2024-12-13 14:52:48 -05:00
=
1080438ad8 feat: changed multi insert into batch insert 2024-12-14 01:19:56 +05:30
eb3acae332 Merge pull request from akhilmhdh/feat/split-project
One slice - 3 Projects
2024-12-13 14:36:58 -05:00
=
a0b3520899 feat: updated rollback 2024-12-14 01:00:12 +05:30
2f6f359ddf Merge pull request from Infisical/misc/operator-namespace-installation
feat: k8 operator namespace installation
2024-12-13 14:10:45 -05:00
=
df8c1e54e0 feat: review changes 2024-12-13 23:50:49 +05:30
=
cac060deff feat: added space 2024-12-13 21:38:44 +05:30
=
47269bc95b feat: resolved undefined redirect 2024-12-13 21:38:44 +05:30
=
8502e9a1d8 feat: removed console log 2024-12-13 21:38:43 +05:30
=
d89eb4fa84 feat: added check in workspace cert api 2024-12-13 21:38:43 +05:30
=
ca7ab4eaf1 feat: resolved typo in access control 2024-12-13 21:38:43 +05:30
=
c57fc5e3f1 feat: fixed review comments 2024-12-13 21:38:43 +05:30
=
9b4e1f561e feat: removed service token from migration and resolved failing migration on groups 2024-12-13 21:38:43 +05:30
=
097fcad5ae fix: resolved failing seed 2024-12-13 21:38:43 +05:30
=
d1547564f9 feat: run through check to all frontend urls 2024-12-13 21:38:43 +05:30
=
24acb98978 feat: project settings hiding 2024-12-13 21:38:42 +05:30
=
0fd8274ff0 feat: added project id mapping logic for cert and kms 2024-12-13 21:38:42 +05:30
=
a857375cc1 feat: fixed migration issues and resolved all routes in frontend 2024-12-13 21:38:42 +05:30
=
69bf9dc20f feat: completed migration 2024-12-13 21:38:42 +05:30
=
5151c91760 feat: check for cmek implemented 2024-12-13 21:38:42 +05:30
=
f12d8b6f89 feat: check for cert manager endpoints 2024-12-13 21:38:42 +05:30
=
695c499448 feat: added type for project and validation check for secret manager specific endpoints 2024-12-13 21:38:42 +05:30
1cbf030e6c Merge remote-tracking branch 'origin/main' into feat/sync-circle-ci-context 2024-12-13 22:34:06 +08:00
dc715cc238 Merge pull request from Infisical/misc/address-high-cpu-usage-from-secret-version-query
misc: address cpu usage issue of secret version query
2024-12-13 08:34:36 -05:00
d873f2e50f misc: address cpu usage issue of secret version query 2024-12-13 20:31:34 +08:00
16ea757928 Merge pull request from Infisical/feat/jwt-auth
feat: jwt auth
2024-12-13 14:15:43 +08:00
8713643bc1 misc: add support for number field values 2024-12-13 14:02:32 +08:00
c35657ed49 misc: addressed review comments 2024-12-13 13:39:23 +08:00
5b4487fae8 add period to secret share text 2024-12-12 16:04:51 -05:00
474731d8ef update share secret text 2024-12-12 16:02:30 -05:00
e9f254f81b Update azure-devops.mdx 2024-12-12 15:36:38 -05:00
639057415f Merge remote-tracking branch 'origin/main' into misc/operator-namespace-installation 2024-12-13 03:49:10 +08:00
c38dae2319 misc: updated version 2024-12-13 03:06:07 +08:00
25191cff38 Merge pull request from Infisical/maidul-update-make-wish
Update make wish text
2024-12-12 10:05:12 -05:00
a6898717f4 update make wish text 2024-12-12 10:01:13 -05:00
36a13d182f requested changes 2024-12-12 06:13:55 +04:00
cc77175188 Merge pull request from Infisical/daniel/plain-to-pylon
feat: remove plain and move to pylon
2024-12-11 19:56:56 -05:00
fcb944d964 Merge pull request from Infisical/omar/eng-1806-add-instance-url-to-email-verification-for-infisical
improvement: Add email footer with instance URL
2024-12-11 19:48:27 -05:00
a8ad8707ac Merge pull request from Infisical/daniel/copy-paste
fix(dashboard): pasting secrets into create secret modal
2024-12-12 03:56:43 +04:00
8b26670d73 fix(k8s): dynamic secret structual change 2024-12-11 22:25:02 +04:00
35d3581e23 fix(k8s): fixed dynamic secret bugs 2024-12-11 22:22:52 +04:00
a7b25f3bd8 misc: addressed module issue 2024-12-12 00:24:36 +08:00
7896b4e85e doc: added documentation 2024-12-12 00:23:06 +08:00
ba5e6fe28a Merge pull request from muhammed-mamun/patch-1
Fix typo in README.md
2024-12-11 10:19:17 -05:00
8d79fa3529 misc: finalized login logic and other ui/ux changes 2024-12-11 22:49:26 +08:00
1a55909b73 Fix typo in README.md
Corrected the typo "Cryptograhic" to "Cryptographic" in the README.md file.
2024-12-11 19:59:06 +06:00
b2efb2845a misc: finalized api endpoint schema 2024-12-11 21:09:25 +08:00
c680030f01 Merge pull request from Infisical/misc/moved-integration-auth-to-params
misc: moved integration auth to params
2024-12-11 19:04:39 +08:00
cf1070c65e misc: moved integration auth to params 2024-12-11 17:56:30 +08:00
f5920f416a Merge remote-tracking branch 'origin' into ssh-certs 2024-12-10 12:46:14 -08:00
3b2154bab4 Add further input validation/sanitization for ssh params 2024-12-10 12:44:08 -08:00
7c8f2e5548 docs + minor fixes 2024-12-10 21:14:13 +01:00
9d9f6ec268 misc: initial ui work 2024-12-11 03:40:21 +08:00
c5816014a6 Add suggested PR review improvements, better validation on ssh cert template modal 2024-12-10 11:34:08 -08:00
a730b16318 fix circleCI name spacing 2024-12-10 20:12:55 +01:00
cc3d132f5d feat(integrations): New CircleCI Context Sync 2024-12-10 20:07:23 +01:00
56aab172d3 feat: added logic for jwt auth login 2024-12-11 00:05:31 +08:00
c8ee06341a feat: finished crud endpoints 2024-12-10 23:10:44 +08:00
e32716c258 improvement: Better group member management ()
* improvement: Better org member management
2024-12-10 14:10:14 +01:00
7f0d27e3dc Merge pull request from Infisical/daniel/improve-project-creation-speed
fix(dashboard): improved project creation speed
2024-12-10 16:33:39 +04:00
48174e2500 security + performance improvements to ssh fns 2024-12-09 22:22:54 -08:00
7cf297344b Move ssh back to project level 2024-12-09 21:36:42 -08:00
5d9b99bee7 Update NewProjectModal.tsx 2024-12-10 07:47:36 +04:00
8fdc438940 feat: remove plain and move to pylon 2024-12-10 07:32:09 +04:00
0edf0dac98 fix(k8-operator): PushSecret CRd causing endless snapshot updates 2024-12-10 04:31:55 +04:00
3c954ea257 set all instances to show URL 2024-12-09 21:46:56 +01:00
84c26581a6 feat: jwt auth setup 2024-12-10 02:41:04 +08:00
7d5aba258a improvement: Add email footer with instance URL 2024-12-09 15:16:05 +01:00
42249726d4 Make PR review adjustments, ssh ca public key endpoint, ssh cert template status 2024-12-08 21:23:00 -08:00
a757ea22a1 fix(k8-operator): improvements for dynamic secrets 2024-12-09 00:22:22 +04:00
74df374998 Update infisicaldynamicsecret-crd.yaml 2024-12-08 23:24:38 +04:00
925a594a1b feat(k8-operator): dynamic secrets status conditions logging 2024-12-08 23:24:30 +04:00
36af975594 docs(k8-operator): k8's dynamic secret docs 2024-12-08 22:42:29 +04:00
ee54d460a0 fix(k8-operator): update charts 2024-12-08 21:30:28 +04:00
3c32d8dd90 fix(k8-operator): helm 2024-12-08 21:30:28 +04:00
9b50d451ec fix(k8-operator): common types support 2024-12-08 21:30:28 +04:00
7ede4e2cf5 fix(k8-operator): moved template 2024-12-08 21:30:28 +04:00
4552f0efa4 feat(k8-operator): dynamic secrets 2024-12-08 21:30:28 +04:00
0d35273857 feat(k8-operator): push secrets 2024-12-08 21:30:09 +04:00
5ad8dab250 fix(k8-operator): resource-based finalizer names 2024-12-08 21:29:45 +04:00
92a80b3314 fix(k8-operator): helm and cleanup 2024-12-08 21:21:14 +04:00
01dcbb0122 updated env slugs 2024-12-07 05:22:21 +04:00
adb0819102 Added RBAC 2024-12-07 05:22:21 +04:00
41ba111a69 Update PROJECT 2024-12-07 05:22:21 +04:00
1b48ce21be Update conditions.go 2024-12-07 05:22:21 +04:00
2f922d6343 fix: requested changes 2024-12-07 05:22:21 +04:00
e67b0540dd cleanup and resource seperation 2024-12-07 05:22:21 +04:00
a78455fde6 remove print 2024-12-07 05:22:21 +04:00
967dac9be6 docs(k8-operator): push secrets 2024-12-07 05:22:21 +04:00
922b245780 feat(k8-operator): push secrets 2024-12-07 05:22:21 +04:00
ec1ce3dc06 Fix type issues 2024-12-05 23:16:31 -08:00
82a4b89bb5 Fix invalid file path for ssh 2024-12-05 23:09:04 -08:00
ff3d8c896b Fix frontend lint issues 2024-12-05 23:06:04 -08:00
6e720c2f64 Add SSH certificate tab + data structure 2024-12-05 23:01:28 -08:00
39f71f9488 feat: k8 operator namespace installation 2024-12-05 23:12:37 +08:00
5b618b07fa Add sign SSH key operation to frontend 2024-12-04 20:13:30 -08:00
a5a1f57284 Fix issued ssh cert defaul ttl 2024-12-04 18:38:14 -08:00
8327f6154e Add openssh dependency onto production Dockerfile 2024-12-03 23:25:22 -08:00
20a9fc113c Update ttl field label on ssh template modal 2024-12-03 23:23:39 -08:00
8edfa9ad0b Improve requested user/host validation for ssh certificate template 2024-12-03 23:22:04 -08:00
00ce755996 Fix type issues 2024-12-03 22:38:29 -08:00
3b2173a098 Add issue SSH certificate modal 2024-12-03 18:36:32 -08:00
07d9398aad Add permissioning to SSH, add publicKey return for SSH CA, polish 2024-12-03 17:38:23 -08:00
4fc8c509ac Finish preliminary loop on SSH certificates 2024-12-02 22:37:23 -08:00
242595fceb changed getCaCerts key from ca-cert -> ca-certs 2024-11-24 21:05:39 +05:30
1859 changed files with 67343 additions and 46443 deletions
.env.example
.github/workflows
.infisicalignoreDockerfile.fips.standalone-infisicalDockerfile.standalone-infisicalREADME.md
backend
DockerfileDockerfile.dev
e2e-test/mocks
package-lock.jsonpackage.json
src
@types
db
ee
routes
services
access-approval-policy
access-approval-request
audit-log-stream
audit-log
certificate-authority-crl
dynamic-secret-lease
dynamic-secret
external-kms
group
identity-project-additional-privilege-v2
identity-project-additional-privilege
ldap-config
license
permission
project-user-additional-privilege
saml-config
scim
secret-approval-policy
secret-approval-request
secret-replication
secret-rotation
secret-scanning
secret-snapshot
ssh-certificate-template
ssh-certificate
ssh
trusted-ip
lib
queue
server
services
app-connection
auth-token
auth
certificate-authority
certificate-template
certificate
cmek
external-migration
group-project
identity-access-token
identity-jwt-auth
identity-oidc-auth
identity-project
identity
integration-auth
integration
org
pki-alert
pki-collection
project-bot
project-env
project-key
project-membership
project-role
project
resource-cleanup
resource-metadata
secret-blind-index
secret-folder
secret-import
secret-sharing
secret-tag
secret-v2-bridge
secret
service-token
slack
smtp
super-admin
user-engagement
webhook
cli
company
docker-compose.dev.yml
docs
api-reference/endpoints
changelog
cli/commands
documentation/platform
images
integrations
mint.json
self-hosting
configuration
reference-architectures
frontend
.dockerignore.eslintrc.js.gitignore.prettierrc
.storybook
DockerfileDockerfile.devREADME.mdcypress.config.js
cypress
eslint.config.jsindex.htmlnext-env.d.tsnext.config.jspackage-lock.jsonpackage.jsonpostcss.config.js
public
scripts
src
components
AddTagPopoverContent
RouteGuard.tsx
analytics
auth
basic
dashboard
features
integrations
license/UpgradePlanModal
mfa
navigation
notifications
organization/CreateOrgModal
permissions
projects
secrets/SecretReferenceDetails
tags/CreateTagModal
utilities
v2
config
const.ts
const
context
ee
global.d.ts
helpers
hoc
withPermission
withProjectPermission
hooks
api
accessApproval
admin
apiKeys
appConnections
auditLogStreams
auditLogs
auth
bots
ca
certificateTemplates
certificates
cmeks
dashboard
dynamicSecret
dynamicSecretLease
externalGroupOrgRoleMappings
groups
identities
identityProjectAdditionalPrivilege
incidentContacts
index.tsx
integrationAuth
integrations
keys
kms
ldapConfig
migration
oidcConfig
organization
pkiAlerts
pkiCollections
policies
projectTemplates
projectUserAdditionalPrivilege
rateLimit
reactQuery.tsx
roles
scim
secretApproval
secretApprovalRequest
secretFolders
secretImports
secretRotation
secretScanning
secretSharing
secretSnapshots
secrets
serviceTokens
sshCa
sshCertificateTemplates
ssoConfig
subscriptions
tags
trustedIps
userEngagement
users
webhooks
workflowIntegrations
workspace
index.tsuseDebounce.tsxuseLeaveConfirm.tsxusePersistentState.tsuseTimedReset.tsxuseToggle.tsx
index.css
layouts
lib/fn
main.tsx
pages
_app.tsx
admin
api
auth
cert-manager
CertAuthDetailsByIDPage
CertificatesPage
PkiCollectionDetailsByIDPage
SettingsPage
layout.tsx
dashboard.tsxindex.tsx
integrations
[id].tsx
azure-app-configuration/oauth2
azure-key-vault/oauth2
bitbucket/oauth2
circleci
details
gcp-secret-manager/oauth2
github/oauth2
heroku/oauth2
netlify/oauth2
vercel/oauth2
kms
login
middlewares
org
[id]
admin
audit-logs
billing
identities/[identityId]
members
memberships/[membershipId]
overview
roles/[roleId]
secret-scanning
secret-sharing
settings
none
organization
AccessManagementPage
AdminPage
AppConnections/GithubOauthCallbackPage
AuditLogsPage
BillingPage
CertManagerOverviewPage
GroupDetailsByIDPage
IdentityDetailsByIDPage
KmsOverviewPage
NoOrgPage
RoleByIDPage
SecretManagerOverviewPage
SecretScanningPage
SecretSharingPage
SettingsPage
SettingsPage.tsx
components
AppConnectionsTab
AuditLogStreamTab
ImportTab
OrgAuthTab
OrgDeleteSection
OrgEncryptionTab
OrgGeneralTab
OrgIncidentContactsSection
OrgNameChangeSection
OrgTabGroup
OrgWorkflowIntegrationTab
ProjectTemplatesTab
index.tsx
route.tsx
SshOverviewPage
UserDetailsByIDPage
layout.tsx
personal-settings.tsx
project
AccessControlPage
IdentityDetailsByIDPage
MemberDetailsByIDPage
RoleDetailsBySlugPage
[id]
allowlist
approval
ca/[caId]
certificates
identities/[identityId]
kms
members
[membershipId]
index.tsx
pki-collections/[collectionId]
roles/[roleSlug]
secret-rotation
secrets
settings
public
root.tsx
secret-manager
IPAllowlistPage
IntegrationsDetailsByIDPage
IntegrationsListPage
OverviewPage
SecretApprovalsPage
SecretDashboardPage
SecretDashboardPage.tsxSecretMainPage.store.tsxSecretMainPage.types.ts
components
ActionBar
CreateSecretForm
DynamicSecretListView
FolderListView
PitDrawer
SecretDropzone
SecretImportListView
SecretListView
SnapshotView
route.tsx
SecretRotationPage
SettingsPage
integrations
AwsParameterStoreAuthorizePage
AwsParameterStoreConfigurePage
AwsSecretManagerAuthorizePage
AwsSecretManagerConfigurePage
AzureAppConfigurationConfigurePage
AzureAppConfigurationOauthCallbackPage
AzureDevopsAuthorizePage
AzureDevopsConfigurePage
AzureKeyVaultAuthorizePage
AzureKeyVaultConfigurePage
AzureKeyVaultOauthCallbackPage
BitbucketConfigurePage
BitbucketOauthCallbackPage
ChecklyAuthorizePage
ChecklyConfigurePage
CircleCIAuthorizePage
CircleCIConfigurePage
Cloud66AuthorizePage
Cloud66ConfigurePage
CloudflarePagesAuthorizePage
CloudflarePagesConfigurePage
CloudflareWorkersAuthorizePage
CloudflareWorkersConfigurePage
CodefreshAuthorizePage
CodefreshConfigurePage
DatabricksAuthorizePage
DatabricksConfigurePage
DigitalOceanAppPlatformAuthorizePage
DigitalOceanAppPlatformConfigurePage
FlyioAuthorizePage
FlyioConfigurePage
GcpSecretManagerAuthorizePage
GcpSecretManagerConfigurePage
GcpSecretManagerOauthCallbackPage
GithubAuthorizePage
GithubConfigurePage
GithubOauthCallbackPage
GitlabAuthorizePage
GitlabConfigurePage
GitlabOauthCallbackPage
HashicorpVaultAuthorizePage
HashicorpVaultConfigurePage
HasuraCloudAuthorizePage
HasuraCloudConfigurePage
HerokuConfigurePage
HerokuOauthCallbackPage
LaravelForgeAuthorizePage
LaravelForgeConfigurePage
NetlifyConfigurePage
NetlifyOauthCallbackPage
NorthflankAuthorizePage
NorthflankConfigurePage
OctopusDeployAuthorizePage
OctopusDeployConfigurePage
QoveryAuthorizePage
QoveryConfigurePage
RailwayAuthorizePage
RailwayConfigurePage
RenderAuthorizePage
RenderConfigurePage
RundeckAuthorizePage
RundeckConfigurePage
SelectIntegrationAuthPage
SupabaseAuthorizePage
SupabaseConfigurePage
TeamcityAuthorizePage
TeamcityConfigurePage
TerraformCloudAuthorizePage
TerraformCloudConfigurePage
TravisCIAuthorizePage
TravisCIConfigurePage
VercelConfigurePage
VercelOauthCallbackPage
WindmillAuthorizePage
WindmillConfigurePage
route-azure-app-configurations-oauth-redirect.tsxroute-azure-key-vault-oauth-redirect.tsxroute-bitbucket-oauth-redirect.tsxroute-gcp-oauth-redirect.tsxroute-github-oauth-redirect.tsxroute-gitlab-oauth-redirect.tsxroute-heroku-oauth-redirect.tsxroute-netlify-oauth-redirect.tsxroute-vercel-oauth-redirect.tsx
layout.tsx
secret-scanning.tsx
share-secret
shared/secret/[id]
signup/sso
ssh
user
routeTree.gen.tsroutes.ts
services
translation.tsx
types
views
IntegrationsPage
Login
Org
OrgAdminPage
Project
SecretApprovalPage
SecretMainPage
components
ActionBar/CreateDynamicSecretForm
SecretReferenceDetails
index.tsx
SecretOverviewPage
SecretRotationPage
Settings
BillingSettingsPage
OrgSettingsPage
PersonalSettingsPage
ProjectSettingsPage
ShareSecretPage
ShareSecretPublicPage
Signup
ViewSecretPublicPage
admin
vite-env.d.ts
tsconfig.app.jsontsconfig.jsontsconfig.node.jsontsconfig.tsbuildinfotsr.config.jsonvite.config.ts
helm-charts/secrets-operator
k8-operator
nginx
package-lock.jsonpackage.jsonstandalone-entrypoint.sh

@ -88,3 +88,20 @@ PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
# App Connections
# aws assume-role
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID=
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY=
# github oauth
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID=
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET=
#github app
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
INF_APP_CONNECTION_GITHUB_APP_SLUG=
INF_APP_CONNECTION_GITHUB_APP_ID=

@ -18,18 +18,18 @@ jobs:
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Setup Node 16
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "20"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: 📦 Install dependencies
run: npm install
working-directory: frontend
- name: 🏗️ Run Type check
run: npm run type:check
run: npm run type:check
working-directory: frontend
- name: 🏗️ Run Link check
run: npm run lint:fix
run: npm run lint:fix
working-directory: frontend

@ -5,6 +5,10 @@ permissions:
id-token: write
contents: read
concurrency:
group: "infisical-core-deployment"
cancel-in-progress: false
jobs:
infisical-tests:
name: Integration tests
@ -97,7 +101,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-gamma-stage
@ -113,10 +117,6 @@ jobs:
steps:
- uses: twingate/github-action@v1
with:
# The Twingate Service Key used to connect Twingate to the proper service
# Learn more about [Twingate Services](https://docs.twingate.com/docs/services)
#
# Required
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Checkout code
uses: actions/checkout@v2
@ -153,12 +153,37 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true
- name: Post slack message
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*Deployment Status Update*: ${{ job.status }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Deployment Status Update*: ${{ job.status }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Application:*\nInfisical Core"
- type: "mrkdwn"
text: "*Instance Type:*\nShared Infisical Cloud"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Region:*\nUS"
- type: "mrkdwn"
text: "*Git Tag:*\n<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"
production-eu:
name: EU production deploy
@ -204,9 +229,34 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true
- name: Post slack message
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*Deployment Status Update*: ${{ job.status }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Deployment Status Update*: ${{ job.status }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Application:*\nInfisical Core"
- type: "mrkdwn"
text: "*Instance Type:*\nShared Infisical Cloud"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Region:*\nEU"
- type: "mrkdwn"
text: "*Git Tag:*\n<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"

@ -7,3 +7,4 @@ docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
docs/mint.json:generic-api-key:651
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134
docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:104

@ -8,7 +8,7 @@ FROM node:20-slim AS base
FROM base AS frontend-dependencies
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
COPY frontend/package.json frontend/package-lock.json ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
@ -23,17 +23,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ENV VITE_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@ -44,20 +43,10 @@ WORKDIR /app
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
@ -137,6 +126,7 @@ RUN apt-get update && apt-get install -y \
freetds-dev \
freetds-bin \
tdsodbc \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Configure ODBC in production
@ -159,14 +149,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ENV INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /
@ -191,4 +178,4 @@ EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
CMD ["./standalone-entrypoint.sh"]

@ -12,7 +12,7 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
COPY frontend/package.json frontend/package-lock.json ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
@ -27,17 +27,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ENV VITE_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ENV VITE_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@ -49,20 +48,10 @@ WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 non-root-user
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
@ -139,7 +128,8 @@ RUN apk --update add \
freetds-dev \
bash \
curl \
git
git \
openssh
# Configure ODBC in production
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
@ -158,14 +148,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ENV INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --from=backend-runner /app /backend
@ -188,4 +175,4 @@ EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
CMD ["./standalone-entrypoint.sh"]

@ -56,7 +56,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
- **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments.
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)**: Inject secrets into applications without modifying any code logic.
### Internal PKI:
### Infisical (Internal) PKI:
- **[Private Certificate Authority](https://infisical.com/docs/documentation/platform/pki/private-ca)**: Create CA hierarchies, configure [certificate templates](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-issuing-certificates) for policy enforcement, and start issuing X.509 certificates.
- **[Certificate Management](https://infisical.com/docs/documentation/platform/pki/certificates)**: Manage the certificate lifecycle from [issuance](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-issuing-certificates) to [revocation](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-revoking-certificates) with support for CRL.
@ -64,12 +64,17 @@ We're on a mission to make security tooling more accessible to everyone, not jus
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
### Key Management (KMS):
### Infisical Key Management System (KMS):
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
- **[Cryptographic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
### Infisical SSH
- **[Signed SSH Certificates](https://infisical.com/docs/documentation/platform/ssh)**: Issue ephemeral SSH credentials for secure, short-lived, and centralized access to infrastructure.
### General Platform:
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)**: Track every action taken on the platform.

@ -7,7 +7,8 @@ WORKDIR /app
RUN apk --update add \
python3 \
make \
g++
g++ \
openssh
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \

@ -17,7 +17,8 @@ RUN apk --update add \
openssl-dev \
python3 \
make \
g++
g++ \
openssh
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \

@ -22,8 +22,10 @@ export const mockQueue = (): TQueueServiceFactory => {
listen: (name, event) => {
events[name] = event;
},
getRepeatableJobs: async () => [],
clearQueue: async () => {},
stopJobById: async () => {},
stopRepeatableJobByJobId: async () => true
stopRepeatableJobByJobId: async () => true,
stopRepeatableJobByKey: async () => true
};
};

@ -26,6 +26,7 @@
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
@ -49,7 +50,6 @@
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
@ -5407,6 +5407,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
"license": "MIT",
"engines": {
"node": ">=14"
}
@ -5546,6 +5547,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.1",
"escape-html": "~1.0.3",
@ -5564,16 +5566,85 @@
}
},
"node_modules/@fastify/static": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
"integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz",
"integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==",
"license": "MIT",
"dependencies": {
"@fastify/accept-negotiator": "^1.0.0",
"@fastify/send": "^2.0.0",
"content-disposition": "^0.5.3",
"fastify-plugin": "^4.0.0",
"glob": "^8.0.1",
"p-limit": "^3.1.0"
"fastq": "^1.17.0",
"glob": "^10.3.4"
}
},
"node_modules/@fastify/static/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@fastify/static/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@fastify/static/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/@fastify/static/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@fastify/static/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/@fastify/swagger": {
@ -5600,6 +5671,20 @@
"yaml": "^2.2.2"
}
},
"node_modules/@fastify/swagger-ui/node_modules/@fastify/static": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
"integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
"license": "MIT",
"dependencies": {
"@fastify/accept-negotiator": "^1.0.0",
"@fastify/send": "^2.0.0",
"content-disposition": "^0.5.3",
"fastify-plugin": "^4.0.0",
"glob": "^8.0.1",
"p-limit": "^3.1.0"
}
},
"node_modules/@google-cloud/kms": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
@ -5678,14 +5763,6 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz",
@ -6071,9 +6148,10 @@
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
},
"node_modules/@lukeed/ms": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
"integrity": "sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
@ -9970,18 +10048,6 @@
"optional": true,
"peer": true
},
"node_modules/@team-plain/typescript-sdk": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-4.6.1.tgz",
"integrity": "sha512-Uy9QJXu9U7bJb6WXL9sArGk7FXPpzdqBd6q8tAF1vexTm8fbTJRqcikTKxGtZmNADt+C2SapH3cApM4oHpO4lQ==",
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"graphql": "^16.6.0",
"zod": "3.22.4"
}
},
"node_modules/@techteamer/ocsp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
@ -13900,9 +13966,9 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@ -13924,7 +13990,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@ -13939,6 +14005,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express-session": {
@ -15180,14 +15250,6 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/graphql": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
"integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
@ -17417,15 +17479,16 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -18412,9 +18475,9 @@
"license": "ISC"
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/path-type": {

@ -134,6 +134,7 @@
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
@ -157,7 +158,6 @@
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",

@ -31,9 +31,12 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
@ -52,6 +55,7 @@ import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-acces
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@ -162,6 +166,7 @@ declare module "fastify" {
identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory;
identityJwtAuth: TIdentityJwtAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
@ -175,6 +180,8 @@ declare module "fastify" {
auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory;
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory;
@ -202,6 +209,7 @@ declare module "fastify" {
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
projectTemplate: TProjectTemplateServiceFactory;
totp: TTotpServiceFactory;
appConnection: TAppConnectionServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

@ -98,6 +98,9 @@ import {
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
TIdentityJwtAuths,
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate,
TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate,
@ -199,6 +202,9 @@ import {
TProjectSlackConfigs,
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectSplitBackfillIds,
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate,
TProjectsUpdate,
TProjectTemplates,
TProjectTemplatesInsert,
@ -212,6 +218,9 @@ import {
TRateLimit,
TRateLimitInsert,
TRateLimitUpdate,
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate,
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
@ -311,6 +320,21 @@ import {
TSlackIntegrations,
TSlackIntegrationsInsert,
TSlackIntegrationsUpdate,
TSshCertificateAuthorities,
TSshCertificateAuthoritiesInsert,
TSshCertificateAuthoritiesUpdate,
TSshCertificateAuthoritySecrets,
TSshCertificateAuthoritySecretsInsert,
TSshCertificateAuthoritySecretsUpdate,
TSshCertificateBodies,
TSshCertificateBodiesInsert,
TSshCertificateBodiesUpdate,
TSshCertificates,
TSshCertificatesInsert,
TSshCertificatesUpdate,
TSshCertificateTemplates,
TSshCertificateTemplatesInsert,
TSshCertificateTemplatesUpdate,
TSuperAdmin,
TSuperAdminInsert,
TSuperAdminUpdate,
@ -342,6 +366,7 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
import {
TExternalGroupOrgRoleMappings,
TExternalGroupOrgRoleMappingsInsert,
@ -372,6 +397,31 @@ declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
TSshCertificateAuthorities,
TSshCertificateAuthoritiesInsert,
TSshCertificateAuthoritiesUpdate
>;
[TableName.SshCertificateAuthoritySecret]: KnexOriginal.CompositeTableType<
TSshCertificateAuthoritySecrets,
TSshCertificateAuthoritySecretsInsert,
TSshCertificateAuthoritySecretsUpdate
>;
[TableName.SshCertificateTemplate]: KnexOriginal.CompositeTableType<
TSshCertificateTemplates,
TSshCertificateTemplatesInsert,
TSshCertificateTemplatesUpdate
>;
[TableName.SshCertificate]: KnexOriginal.CompositeTableType<
TSshCertificates,
TSshCertificatesInsert,
TSshCertificatesUpdate
>;
[TableName.SshCertificateBody]: KnexOriginal.CompositeTableType<
TSshCertificateBodies,
TSshCertificateBodiesInsert,
TSshCertificateBodiesUpdate
>;
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
TCertificateAuthorities,
TCertificateAuthoritiesInsert,
@ -590,6 +640,11 @@ declare module "knex/types/tables" {
TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate
>;
[TableName.IdentityJwtAuth]: KnexOriginal.CompositeTableType<
TIdentityJwtAuths,
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate
>;
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
@ -830,5 +885,20 @@ declare module "knex/types/tables" {
TProjectTemplatesUpdate
>;
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
[TableName.ProjectSplitBackfillIds]: KnexOriginal.CompositeTableType<
TProjectSplitBackfillIds,
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate
>;
[TableName.ResourceMetadata]: KnexOriginal.CompositeTableType<
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate
>;
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
TAppConnections,
TAppConnectionsInsert,
TAppConnectionsUpdate
>;
}
}

@ -0,0 +1,34 @@
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.IdentityJwtAuth))) {
await knex.schema.createTable(TableName.IdentityJwtAuth, (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.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("configurationType").notNullable();
t.string("jwksUrl").notNullable();
t.binary("encryptedJwksCaCert").notNullable();
t.binary("encryptedPublicKeys").notNullable();
t.string("boundIssuer").notNullable();
t.string("boundAudiences").notNullable();
t.jsonb("boundClaims").notNullable();
t.string("boundSubject").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.IdentityJwtAuth);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityJwtAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityJwtAuth);
}

@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.index("folderId");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.dropIndex("folderId");
});
}
}

@ -0,0 +1,297 @@
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { v4 as uuidV4 } from "uuid";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ProjectType, TableName } from "../schemas";
/* eslint-disable no-await-in-loop,@typescript-eslint/ban-ts-comment */
const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => {
const newProjectId = uuidV4();
const project = await knex(TableName.Project).where("id", projectId).first();
await knex(TableName.Project).insert({
...project,
type: projectType,
// @ts-ignore id is required
id: newProjectId,
slug: slugify(`${project?.name}-${alphaNumericNanoId(4)}`)
});
const customRoleMapping: Record<string, string> = {};
const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId);
if (projectCustomRoles.length) {
await knex.batchInsert(
TableName.ProjectRoles,
projectCustomRoles.map((el) => {
const id = uuidV4();
customRoleMapping[el.id] = id;
return {
...el,
id,
projectId: newProjectId,
permissions: el.permissions ? JSON.stringify(el.permissions) : el.permissions
};
})
);
}
const groupMembershipMapping: Record<string, string> = {};
const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId);
if (groupMemberships.length) {
await knex.batchInsert(
TableName.GroupProjectMembership,
groupMemberships.map((el) => {
const id = uuidV4();
groupMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn(
"projectMembershipId",
groupMemberships.map((el) => el.id)
);
if (groupMembershipRoles.length) {
await knex.batchInsert(
TableName.GroupProjectMembershipRole,
groupMembershipRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = groupMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const identityProjectMembershipMapping: Record<string, string> = {};
const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId);
if (identities.length) {
await knex.batchInsert(
TableName.IdentityProjectMembership,
identities.map((el) => {
const id = uuidV4();
identityProjectMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn(
"projectMembershipId",
identities.map((el) => el.id)
);
if (identitiesRoles.length) {
await knex.batchInsert(
TableName.IdentityProjectMembershipRole,
identitiesRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const projectMembershipMapping: Record<string, string> = {};
const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId);
if (projectUserMembers.length) {
await knex.batchInsert(
TableName.ProjectMembership,
projectUserMembers.map((el) => {
const id = uuidV4();
projectMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn(
"projectMembershipId",
projectUserMembers.map((el) => el.id)
);
if (membershipRoles.length) {
await knex.batchInsert(
TableName.ProjectUserMembershipRole,
membershipRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = projectMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true);
if (kmsKeys.length) {
await knex.batchInsert(
TableName.KmsKey,
kmsKeys.map((el) => {
const id = uuidV4();
const slug = slugify(alphaNumericNanoId(8).toLowerCase());
return { ...el, id, slug, projectId: newProjectId };
})
);
}
const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first();
if (projectBot) {
const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId };
await knex(TableName.ProjectBot).insert(newProjectBot);
}
const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId);
if (projectKeys.length) {
await knex.batchInsert(
TableName.ProjectKeys,
projectKeys.map((el) => {
const id = uuidV4();
return { ...el, id, projectId: newProjectId };
})
);
}
return newProjectId;
};
const BATCH_SIZE = 500;
export async function up(knex: Knex): Promise<void> {
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
if (!hasSplitMappingTable) {
await knex.schema.createTable(TableName.ProjectSplitBackfillIds, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("sourceProjectId", 36).notNullable();
t.foreign("sourceProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.string("destinationProjectType").notNullable();
t.string("destinationProjectId", 36).notNullable();
t.foreign("destinationProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
if (!hasTypeColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("type");
});
let projectsToBeTyped;
do {
// eslint-disable-next-line no-await-in-loop
projectsToBeTyped = await knex(TableName.Project).whereNull("type").limit(BATCH_SIZE).select("id");
if (projectsToBeTyped.length) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.Project)
.whereIn(
"id",
projectsToBeTyped.map((el) => el.id)
)
.update({ type: ProjectType.SecretManager });
}
} while (projectsToBeTyped.length > 0);
const projectsWithCertificates = await knex(TableName.CertificateAuthority)
.distinct("projectId")
.select("projectId");
/* eslint-disable no-await-in-loop,no-param-reassign */
for (const { projectId } of projectsWithCertificates) {
const newProjectId = await newProject(knex, projectId, ProjectType.CertificateManager);
await knex(TableName.CertificateAuthority).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.PkiAlert).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.PkiCollection).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.ProjectSplitBackfillIds).insert({
sourceProjectId: projectId,
destinationProjectType: ProjectType.CertificateManager,
destinationProjectId: newProjectId
});
}
const projectsWithCmek = await knex(TableName.KmsKey)
.where("isReserved", false)
.whereNotNull("projectId")
.distinct("projectId")
.select("projectId");
for (const { projectId } of projectsWithCmek) {
if (projectId) {
const newProjectId = await newProject(knex, projectId, ProjectType.KMS);
await knex(TableName.KmsKey)
.where({
isReserved: false,
projectId
})
.update({ projectId: newProjectId });
await knex(TableName.ProjectSplitBackfillIds).insert({
sourceProjectId: projectId,
destinationProjectType: ProjectType.KMS,
destinationProjectId: newProjectId
});
}
}
/* eslint-enable */
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("type").notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
if (hasTypeColumn && hasSplitMappingTable) {
const splitProjectMappings = await knex(TableName.ProjectSplitBackfillIds).where({});
const certMapping = splitProjectMappings.filter(
(el) => el.destinationProjectType === ProjectType.CertificateManager
);
/* eslint-disable no-await-in-loop */
for (const project of certMapping) {
await knex(TableName.CertificateAuthority)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
await knex(TableName.PkiAlert)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
await knex(TableName.PkiCollection)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
}
/* eslint-enable */
const kmsMapping = splitProjectMappings.filter((el) => el.destinationProjectType === ProjectType.KMS);
/* eslint-disable no-await-in-loop */
for (const project of kmsMapping) {
await knex(TableName.KmsKey)
.where({
isReserved: false,
projectId: project.destinationProjectId
})
.update({ projectId: project.sourceProjectId });
}
/* eslint-enable */
await knex(TableName.ProjectMembership)
.whereIn(
"projectId",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex(TableName.ProjectRoles)
.whereIn(
"projectId",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex(TableName.Project)
.whereIn(
"id",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("type");
});
}
if (hasSplitMappingTable) {
await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds);
}
}

@ -0,0 +1,99 @@
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.SshCertificateAuthority))) {
await knex.schema.createTable(TableName.SshCertificateAuthority, (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.string("status").notNullable(); // active / disabled
t.string("friendlyName").notNullable();
t.string("keyAlgorithm").notNullable();
});
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
}
if (!(await knex.schema.hasTable(TableName.SshCertificateAuthoritySecret))) {
await knex.schema.createTable(TableName.SshCertificateAuthoritySecret, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("sshCaId").notNullable().unique();
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
t.binary("encryptedPrivateKey").notNullable();
});
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
}
if (!(await knex.schema.hasTable(TableName.SshCertificateTemplate))) {
await knex.schema.createTable(TableName.SshCertificateTemplate, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("sshCaId").notNullable();
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
t.string("status").notNullable(); // active / disabled
t.string("name").notNullable();
t.string("ttl").notNullable();
t.string("maxTTL").notNullable();
t.specificType("allowedUsers", "text[]").notNullable();
t.specificType("allowedHosts", "text[]").notNullable();
t.boolean("allowUserCertificates").notNullable();
t.boolean("allowHostCertificates").notNullable();
t.boolean("allowCustomKeyIds").notNullable();
});
await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
}
if (!(await knex.schema.hasTable(TableName.SshCertificate))) {
await knex.schema.createTable(TableName.SshCertificate, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("sshCaId").notNullable();
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
t.uuid("sshCertificateTemplateId");
t.foreign("sshCertificateTemplateId")
.references("id")
.inTable(TableName.SshCertificateTemplate)
.onDelete("SET NULL");
t.string("serialNumber").notNullable().unique();
t.string("certType").notNullable(); // user or host
t.specificType("principals", "text[]").notNullable();
t.string("keyId").notNullable();
t.datetime("notBefore").notNullable();
t.datetime("notAfter").notNullable();
});
await createOnUpdateTrigger(knex, TableName.SshCertificate);
}
if (!(await knex.schema.hasTable(TableName.SshCertificateBody))) {
await knex.schema.createTable(TableName.SshCertificateBody, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("sshCertId").notNullable().unique();
t.foreign("sshCertId").references("id").inTable(TableName.SshCertificate).onDelete("CASCADE");
t.binary("encryptedCertificate").notNullable();
});
await createOnUpdateTrigger(knex, TableName.SshCertificateBody);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SshCertificateBody);
await dropOnUpdateTrigger(knex, TableName.SshCertificateBody);
await knex.schema.dropTableIfExists(TableName.SshCertificate);
await dropOnUpdateTrigger(knex, TableName.SshCertificate);
await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate);
await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthoritySecret);
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthority);
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
}

@ -0,0 +1,40 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ResourceMetadata))) {
await knex.schema.createTable(TableName.ResourceMetadata, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("key").notNullable();
tb.string("value", 1020).notNullable();
tb.uuid("orgId").notNullable();
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
tb.uuid("userId");
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
tb.uuid("identityId");
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
tb.uuid("secretId");
tb.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
tb.timestamps(true, true, true);
});
}
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (!hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.jsonb("secretMetadata");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ResourceMetadata);
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.dropColumn("secretMetadata");
});
}
}

@ -0,0 +1,28 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.AppConnection))) {
await knex.schema.createTable(TableName.AppConnection, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description");
t.string("app").notNullable();
t.string("method").notNullable();
t.binary("encryptedCredentials").notNullable();
t.integer("version").defaultTo(1).notNullable();
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.AppConnection);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.AppConnection);
await dropOnUpdateTrigger(knex, TableName.AppConnection);
}

@ -0,0 +1,49 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// find any duplicate group names within organizations
const duplicates = await knex(TableName.Groups)
.select("orgId", "name")
.count("* as count")
.groupBy("orgId", "name")
.having(knex.raw("count(*) > 1"));
// for each set of duplicates, update all but one with a numbered suffix
for await (const duplicate of duplicates) {
const groups = await knex(TableName.Groups)
.select("id", "name")
.where({
orgId: duplicate.orgId,
name: duplicate.name
})
.orderBy("createdAt", "asc"); // keep original name for oldest group
// skip the first (oldest) group, rename others with numbered suffix
for (let i = 1; i < groups.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.Groups)
.where("id", groups[i].id)
.update({
name: `${groups[i].name} (${i})`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TS doesn't know about Knex's timestamp types
updatedAt: new Date()
});
}
}
// add the unique constraint
await knex.schema.alterTable(TableName.Groups, (t) => {
t.unique(["orgId", "name"]);
});
}
export async function down(knex: Knex): Promise<void> {
// Remove the unique constraint
await knex.schema.alterTable(TableName.Groups, (t) => {
t.dropUnique(["orgId", "name"]);
});
}

@ -0,0 +1,33 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization");
const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization");
await knex.schema.alterTable(TableName.Project, (t) => {
if (!hasEnforceCapitalizationCol) {
t.boolean("enforceCapitalization").defaultTo(false).notNullable();
}
if (hasAutoCapitalizationCol) {
t.boolean("autoCapitalization").defaultTo(false).alter();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization");
const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization");
await knex.schema.alterTable(TableName.Project, (t) => {
if (hasEnforceCapitalizationCol) {
t.dropColumn("enforceCapitalization");
}
if (hasAutoCapitalizationCol) {
t.boolean("autoCapitalization").defaultTo(true).alter();
}
});
}

@ -0,0 +1,27 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const AppConnectionsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
app: z.string(),
method: z.string(),
encryptedCredentials: zodBuffer,
version: z.number().default(1),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
export type TAppConnectionsInsert = Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>;
export type TAppConnectionsUpdate = Partial<Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>>;

@ -0,0 +1,33 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const IdentityJwtAuthsSchema = 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(),
identityId: z.string().uuid(),
configurationType: z.string(),
jwksUrl: z.string(),
encryptedJwksCaCert: zodBuffer,
encryptedPublicKeys: zodBuffer,
boundIssuer: z.string(),
boundAudiences: z.string(),
boundClaims: z.unknown(),
boundSubject: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityJwtAuths = z.infer<typeof IdentityJwtAuthsSchema>;
export type TIdentityJwtAuthsInsert = Omit<z.input<typeof IdentityJwtAuthsSchema>, TImmutableDBKeys>;
export type TIdentityJwtAuthsUpdate = Partial<Omit<z.input<typeof IdentityJwtAuthsSchema>, TImmutableDBKeys>>;

@ -30,6 +30,7 @@ export * from "./identity-access-tokens";
export * from "./identity-aws-auths";
export * from "./identity-azure-auths";
export * from "./identity-gcp-auths";
export * from "./identity-jwt-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-metadata";
export * from "./identity-oidc-auths";
@ -64,11 +65,13 @@ export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./project-slack-configs";
export * from "./project-split-backfill-ids";
export * from "./project-templates";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";
export * from "./rate-limit";
export * from "./resource-metadata";
export * from "./saml-configs";
export * from "./scim-tokens";
export * from "./secret-approval-policies";
@ -105,6 +108,11 @@ export * from "./secrets";
export * from "./secrets-v2";
export * from "./service-tokens";
export * from "./slack-integrations";
export * from "./ssh-certificate-authorities";
export * from "./ssh-certificate-authority-secrets";
export * from "./ssh-certificate-bodies";
export * from "./ssh-certificate-templates";
export * from "./ssh-certificates";
export * from "./super-admin";
export * from "./totp-configs";
export * from "./trusted-ips";

@ -2,6 +2,11 @@ import { z } from "zod";
export enum TableName {
Users = "users",
SshCertificateAuthority = "ssh_certificate_authorities",
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
SshCertificateTemplate = "ssh_certificate_templates",
SshCertificate = "ssh_certificates",
SshCertificateBody = "ssh_certificate_bodies",
CertificateAuthority = "certificate_authorities",
CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs",
@ -68,12 +73,14 @@ export enum TableName {
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths",
IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
// used by both identity and users
IdentityMetadata = "identity_metadata",
ResourceMetadata = "resource_metadata",
ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies",
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
@ -105,6 +112,7 @@ export enum TableName {
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
ProjectSplitBackfillIds = "project_split_backfill_ids",
// junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction",
@ -122,7 +130,8 @@ export enum TableName {
KmsKeyVersion = "kms_key_versions",
WorkflowIntegrations = "workflow_integrations",
SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs"
ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
@ -196,5 +205,22 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth"
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth"
}
export enum ProjectType {
SecretManager = "secret-manager",
CertificateManager = "cert-manager",
KMS = "kms",
SSH = "ssh"
}
export enum ActionProjectType {
SecretManager = ProjectType.SecretManager,
CertificateManager = ProjectType.CertificateManager,
KMS = ProjectType.KMS,
SSH = ProjectType.SSH,
// project operations that happen on all types
Any = "any"
}

@ -0,0 +1,21 @@
// 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 ProjectSplitBackfillIdsSchema = z.object({
id: z.string().uuid(),
sourceProjectId: z.string(),
destinationProjectType: z.string(),
destinationProjectId: z.string()
});
export type TProjectSplitBackfillIds = z.infer<typeof ProjectSplitBackfillIdsSchema>;
export type TProjectSplitBackfillIdsInsert = Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>;
export type TProjectSplitBackfillIdsUpdate = Partial<
Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>
>;

@ -13,7 +13,7 @@ export const ProjectsSchema = z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
autoCapitalization: z.boolean().default(true).nullable().optional(),
autoCapitalization: z.boolean().default(false).nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
@ -24,7 +24,9 @@ export const ProjectsSchema = z.object({
auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional()
description: z.string().nullable().optional(),
type: z.string(),
enforceCapitalization: z.boolean().default(false)
});
export type TProjects = z.infer<typeof ProjectsSchema>;

@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ResourceMetadataSchema = z.object({
id: z.string().uuid(),
key: z.string(),
value: z.string(),
orgId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
identityId: z.string().uuid().nullable().optional(),
secretId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
export type TResourceMetadataInsert = Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>;
export type TResourceMetadataUpdate = Partial<Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>>;

@ -24,7 +24,8 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({
requestId: z.string().uuid(),
op: z.string(),
secretId: z.string().uuid().nullable().optional(),
secretVersion: z.string().uuid().nullable().optional()
secretVersion: z.string().uuid().nullable().optional(),
secretMetadata: z.unknown().nullable().optional()
});
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;

@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SshCertificateAuthoritiesSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string(),
status: z.string(),
friendlyName: z.string(),
keyAlgorithm: z.string()
});
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
export type TSshCertificateAuthoritiesInsert = Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>;
export type TSshCertificateAuthoritiesUpdate = Partial<
Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>
>;

@ -0,0 +1,27 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SshCertificateAuthoritySecretsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
sshCaId: z.string().uuid(),
encryptedPrivateKey: zodBuffer
});
export type TSshCertificateAuthoritySecrets = z.infer<typeof SshCertificateAuthoritySecretsSchema>;
export type TSshCertificateAuthoritySecretsInsert = Omit<
z.input<typeof SshCertificateAuthoritySecretsSchema>,
TImmutableDBKeys
>;
export type TSshCertificateAuthoritySecretsUpdate = Partial<
Omit<z.input<typeof SshCertificateAuthoritySecretsSchema>, TImmutableDBKeys>
>;

@ -0,0 +1,22 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SshCertificateBodiesSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
sshCertId: z.string().uuid(),
encryptedCertificate: zodBuffer
});
export type TSshCertificateBodies = z.infer<typeof SshCertificateBodiesSchema>;
export type TSshCertificateBodiesInsert = Omit<z.input<typeof SshCertificateBodiesSchema>, TImmutableDBKeys>;
export type TSshCertificateBodiesUpdate = Partial<Omit<z.input<typeof SshCertificateBodiesSchema>, TImmutableDBKeys>>;

@ -0,0 +1,30 @@
// 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 SshCertificateTemplatesSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
sshCaId: z.string().uuid(),
status: z.string(),
name: z.string(),
ttl: z.string(),
maxTTL: z.string(),
allowedUsers: z.string().array(),
allowedHosts: z.string().array(),
allowUserCertificates: z.boolean(),
allowHostCertificates: z.boolean(),
allowCustomKeyIds: z.boolean()
});
export type TSshCertificateTemplates = z.infer<typeof SshCertificateTemplatesSchema>;
export type TSshCertificateTemplatesInsert = Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>;
export type TSshCertificateTemplatesUpdate = Partial<
Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>
>;

@ -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 SshCertificatesSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
sshCaId: z.string().uuid(),
sshCertificateTemplateId: z.string().uuid().nullable().optional(),
serialNumber: z.string(),
certType: z.string(),
principals: z.string().array(),
keyId: z.string(),
notBefore: z.date(),
notAfter: z.date()
});
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
export type TSshCertificatesInsert = Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>;
export type TSshCertificatesUpdate = Partial<Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>>;

@ -4,7 +4,7 @@ import { Knex } from "knex";
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@ -24,6 +24,7 @@ export async function seed(knex: Knex): Promise<void> {
name: seedData1.project.name,
orgId: seedData1.organization.id,
slug: "first-project",
type: ProjectType.SecretManager,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.project.id

@ -1,6 +1,6 @@
import { Knex } from "knex";
import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas";
import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@ -16,6 +16,7 @@ export async function seed(knex: Knex): Promise<void> {
orgId: seedData1.organization.id,
slug: seedData1.projectV3.slug,
version: ProjectVersion.V3,
type: ProjectType.SecretManager,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.projectV3.id

@ -1,6 +1,7 @@
import { z } from "zod";
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
import { EFilterReturnedUsers } from "@app/ee/services/group/group-types";
import { GROUPS } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -151,7 +152,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search)
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search),
filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers)
}),
response: {
200: z.object({
@ -164,7 +166,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
})
.merge(
z.object({
isPartOfGroup: z.boolean()
isPartOfGroup: z.boolean(),
joinedGroupAt: z.date().nullable()
})
)
.array(),

@ -22,9 +22,13 @@ import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-rou
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
import { registerSecretRotationRouter } from "./secret-rotation-router";
import { registerSecretRouter } from "./secret-router";
import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSecretVersionRouter } from "./secret-version-router";
import { registerSnapshotRouter } from "./snapshot-router";
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
import { registerSshCertRouter } from "./ssh-certificate-router";
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
import { registerTrustedIpRouter } from "./trusted-ip-router";
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
@ -68,6 +72,15 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/pki" }
);
await server.register(
async (sshRouter) => {
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
},
{ prefix: "/ssh" }
);
await server.register(
async (ssoRouter) => {
await ssoRouter.register(registerSamlRouter);
@ -80,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretRouter, { prefix: "/secrets" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(registerGroupRouter, { prefix: "/groups" });
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });

@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
"Please choose a different slug, the slug you have entered is reserved"
),
name: z.string().trim(),
description: z.string().trim().optional(),
description: z.string().trim().nullish(),
permissions: z.any().array()
}),
response: {
@ -95,7 +95,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
)
.optional(),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
description: z.string().trim().nullish(),
permissions: z.any().array().optional()
}),
response: {

@ -39,7 +39,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
)
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
}),
response: {
@ -95,7 +95,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.UPDATE.slug)
.optional(),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {

@ -84,7 +84,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
samlConfig.audience = `spn:${ssoConfig.issuer}`;
}
}
if (ssoConfig.authProvider === SamlProviders.GOOGLE_SAML) {
if (
ssoConfig.authProvider === SamlProviders.GOOGLE_SAML ||
ssoConfig.authProvider === SamlProviders.AUTH0_SAML
) {
samlConfig.wantAssertionsSigned = false;
}
@ -123,7 +126,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
`email: ${email} firstName: ${profile.firstName as string}`
);
throw new Error("Invalid saml request. Missing email or first name");
throw new BadRequestError({
message:
"Missing email or first name. Please double check your SAML attribute mapping for the selected provider."
});
}
const userMetadata = Object.keys(profile.attributes || {})

@ -12,6 +12,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
UsersSchema.pick({
@ -274,6 +275,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.extend({
op: z.string(),
tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish(),
secret: z
.object({
id: z.string(),
@ -291,7 +293,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional(),
tags: tagSchema
tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish()
})
.optional()
})

@ -0,0 +1,71 @@
import z from "zod";
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
import { RAW_SECRETS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const AccessListEntrySchema = z
.object({
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
id: z.string(),
membershipId: z.string(),
name: z.string()
})
.array();
export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:secretName/access-list",
config: {
rateLimit: readLimit
},
schema: {
description: "Get list of users, machine identities, and groups with access to a secret",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName)
}),
querystring: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId),
environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.GET_ACCESS_LIST.secretPath)
}),
response: {
200: z.object({
groups: AccessListEntrySchema,
identities: AccessListEntrySchema,
users: AccessListEntrySchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretName } = req.params;
const { secretPath, environment, workspaceId: projectId } = req.query;
return server.services.secret.getSecretAccessList({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
projectId,
secretName
});
}
});
};

@ -1,9 +1,13 @@
import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
import {
SecretScanningResolvedStatus,
SecretScanningRiskStatus
} from "@app/ee/services/secret-scanning/secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
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";
@ -97,6 +101,45 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
}
});
server.route({
url: "/organization/:organizationId/risks/export",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({ organizationId: z.string().trim() }),
querystring: z.object({
repositoryNames: z
.string()
.optional()
.nullable()
.transform((val) => (val ? val.split(",") : undefined)),
resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional()
}),
response: {
200: z.object({
risks: SecretScanningGitRisksSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const risks = await server.services.secretScanning.getAllRisksByOrg({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
filter: {
repositoryNames: req.query.repositoryNames,
resolvedStatus: req.query.resolvedStatus
}
});
return { risks };
}
});
server.route({
url: "/organization/:organizationId/risks",
method: "GET",
@ -105,20 +148,46 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
},
schema: {
params: z.object({ organizationId: z.string().trim() }),
querystring: z.object({
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(20000).default(100),
orderBy: z.enum(["createdAt", "name"]).default("createdAt"),
orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.DESC),
repositoryNames: z
.string()
.optional()
.nullable()
.transform((val) => (val ? val.split(",") : undefined)),
resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional()
}),
response: {
200: z.object({ risks: SecretScanningGitRisksSchema.array() })
200: z.object({
risks: SecretScanningGitRisksSchema.array(),
totalCount: z.number(),
repos: z.array(z.string())
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { risks } = await server.services.secretScanning.getRisksByOrg({
const { risks, totalCount, repos } = await server.services.secretScanning.getRisksByOrg({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId
orgId: req.params.organizationId,
filter: {
limit: req.query.limit,
offset: req.query.offset,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
repositoryNames: req.query.repositoryNames,
resolvedStatus: req.query.resolvedStatus
}
});
return { risks };
return { risks, totalCount, repos };
}
});

@ -0,0 +1,279 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
import { SSH_CERTIFICATE_AUTHORITIES } 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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create SSH CA",
body: z.object({
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
keyAlgorithm: z
.nativeEnum(CertKeyAlgorithm)
.default(CertKeyAlgorithm.RSA_2048)
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
}),
response: {
200: z.object({
ca: sanitizedSshCa.extend({
publicKey: z.string()
})
})
}
},
handler: async (req) => {
const ca = await server.services.sshCertificateAuthority.createSshCa({
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: ca.projectId,
event: {
type: EventType.CREATE_SSH_CA,
metadata: {
sshCaId: ca.id,
friendlyName: ca.friendlyName
}
}
});
return {
ca
};
}
});
server.route({
method: "GET",
url: "/:sshCaId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get SSH CA",
params: z.object({
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET.sshCaId)
}),
response: {
200: z.object({
ca: sanitizedSshCa.extend({
publicKey: z.string()
})
})
}
},
handler: async (req) => {
const ca = await server.services.sshCertificateAuthority.getSshCaById({
caId: req.params.sshCaId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_SSH_CA,
metadata: {
sshCaId: ca.id,
friendlyName: ca.friendlyName
}
}
});
return {
ca
};
}
});
server.route({
method: "GET",
url: "/:sshCaId/public-key",
config: {
rateLimit: readLimit
},
schema: {
description: "Get public key of SSH CA",
params: z.object({
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_PUBLIC_KEY.sshCaId)
}),
response: {
200: z.string()
}
},
handler: async (req) => {
const publicKey = await server.services.sshCertificateAuthority.getSshCaPublicKey({
caId: req.params.sshCaId
});
return publicKey;
}
});
server.route({
method: "PATCH",
url: "/:sshCaId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update SSH CA",
params: z.object({
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId)
}),
body: z.object({
friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName),
status: z.nativeEnum(SshCaStatus).optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
}),
response: {
200: z.object({
ca: sanitizedSshCa.extend({
publicKey: z.string()
})
})
}
},
handler: async (req) => {
const ca = await server.services.sshCertificateAuthority.updateSshCaById({
caId: req.params.sshCaId,
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: ca.projectId,
event: {
type: EventType.UPDATE_SSH_CA,
metadata: {
sshCaId: ca.id,
friendlyName: ca.friendlyName,
status: ca.status as SshCaStatus
}
}
});
return {
ca
};
}
});
server.route({
method: "DELETE",
url: "/:sshCaId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete SSH CA",
params: z.object({
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.DELETE.sshCaId)
}),
response: {
200: z.object({
ca: sanitizedSshCa
})
}
},
handler: async (req) => {
const ca = await server.services.sshCertificateAuthority.deleteSshCaById({
caId: req.params.sshCaId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.DELETE_SSH_CA,
metadata: {
sshCaId: ca.id,
friendlyName: ca.friendlyName
}
}
});
return {
ca
};
}
});
server.route({
method: "GET",
url: "/:sshCaId/certificate-templates",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get list of certificate templates for the SSH CA",
params: z.object({
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_CERTIFICATE_TEMPLATES.sshCaId)
}),
response: {
200: z.object({
certificateTemplates: sanitizedSshCertificateTemplate.array()
})
}
},
handler: async (req) => {
const { certificateTemplates, ca } = await server.services.sshCertificateAuthority.getSshCaCertificateTemplates({
caId: req.params.sshCaId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES,
metadata: {
sshCaId: ca.id,
friendlyName: ca.friendlyName
}
}
});
return {
certificateTemplates
};
}
});
};

@ -0,0 +1,164 @@
import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/sign",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign SSH public key",
body: z.object({
certificateTemplateId: z
.string()
.trim()
.min(1)
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certificateTemplateId),
publicKey: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.publicKey),
certType: z
.nativeEnum(SshCertType)
.default(SshCertType.USER)
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certType),
principals: z
.array(z.string().transform((val) => val.trim()))
.nonempty("Principals array must not be empty")
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.principals),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.ttl),
keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.keyId)
}),
response: {
200: z.object({
serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.serialNumber),
signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.signedKey)
})
}
},
handler: async (req) => {
const { serialNumber, signedPublicKey, certificateTemplate, ttl, keyId } =
await server.services.sshCertificateAuthority.signSshKey({
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,
orgId: req.permission.orgId,
event: {
type: EventType.SIGN_SSH_KEY,
metadata: {
certificateTemplateId: certificateTemplate.id,
certType: req.body.certType,
principals: req.body.principals,
ttl: String(ttl),
keyId
}
}
});
return {
serialNumber,
signedKey: signedPublicKey
};
}
});
server.route({
method: "POST",
url: "/issue",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Issue SSH credentials (certificate + key)",
body: z.object({
certificateTemplateId: z
.string()
.trim()
.min(1)
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
keyAlgorithm: z
.nativeEnum(CertKeyAlgorithm)
.default(CertKeyAlgorithm.RSA_2048)
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
certType: z
.nativeEnum(SshCertType)
.default(SshCertType.USER)
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certType),
principals: z
.array(z.string().transform((val) => val.trim()))
.nonempty("Principals array must not be empty")
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.principals),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.ttl),
keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyId)
}),
response: {
200: z.object({
serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.serialNumber),
signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.signedKey),
privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
keyAlgorithm: z
.nativeEnum(CertKeyAlgorithm)
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
})
}
},
handler: async (req) => {
const { serialNumber, signedPublicKey, privateKey, publicKey, certificateTemplate, ttl, keyId } =
await server.services.sshCertificateAuthority.issueSshCreds({
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,
orgId: req.permission.orgId,
event: {
type: EventType.ISSUE_SSH_CREDS,
metadata: {
certificateTemplateId: certificateTemplate.id,
keyAlgorithm: req.body.keyAlgorithm,
certType: req.body.certType,
principals: req.body.principals,
ttl: String(ttl),
keyId
}
}
});
return {
serialNumber,
signedKey: signedPublicKey,
privateKey,
publicKey,
keyAlgorithm: req.body.keyAlgorithm
};
}
});
};

@ -0,0 +1,258 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import {
isValidHostPattern,
isValidUserPattern
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
import { SSH_CERTIFICATE_TEMPLATES } 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";
export const registerSshCertificateTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:certificateTemplateId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
}),
response: {
200: sanitizedSshCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.sshCertificateTemplate.getSshCertTemplate({
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id
}
}
});
return certificateTemplate;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z
.object({
sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId),
name: z
.string()
.min(1)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Name must be a valid slug"
})
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.name),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.default("1h")
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.ttl),
maxTTL: z
.string()
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
.default("30d")
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.maxTTL),
allowedUsers: z
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedUsers),
allowedHosts: z
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedHosts),
allowUserCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowUserCertificates),
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
})
.refine((data) => ms(data.maxTTL) > ms(data.ttl), {
message: "Max TLL must be greater than TTL",
path: ["maxTTL"]
}),
response: {
200: sanitizedSshCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { certificateTemplate, ca } = await server.services.sshCertificateTemplate.createSshCertTemplate({
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: ca.projectId,
event: {
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
sshCaId: ca.id,
name: certificateTemplate.name,
ttl: certificateTemplate.ttl,
maxTTL: certificateTemplate.maxTTL,
allowedUsers: certificateTemplate.allowedUsers,
allowedHosts: certificateTemplate.allowedHosts,
allowUserCertificates: certificateTemplate.allowUserCertificates,
allowHostCertificates: certificateTemplate.allowHostCertificates,
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
}
}
});
return certificateTemplate;
}
});
server.route({
method: "PATCH",
url: "/:certificateTemplateId",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
status: z.nativeEnum(SshCertTemplateStatus).optional(),
name: z
.string()
.min(1)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.name),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.ttl),
maxTTL: z
.string()
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
.optional()
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.maxTTL),
allowedUsers: z
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
.optional()
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedUsers),
allowedHosts: z
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
.optional()
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedHosts),
allowUserCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowUserCertificates),
allowHostCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowHostCertificates),
allowCustomKeyIds: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowCustomKeyIds)
}),
params: z.object({
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
}),
response: {
200: sanitizedSshCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { certificateTemplate, projectId } = await server.services.sshCertificateTemplate.updateSshCertTemplate({
...req.body,
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE,
metadata: {
status: certificateTemplate.status as SshCertTemplateStatus,
certificateTemplateId: certificateTemplate.id,
sshCaId: certificateTemplate.sshCaId,
name: certificateTemplate.name,
ttl: certificateTemplate.ttl,
maxTTL: certificateTemplate.maxTTL,
allowedUsers: certificateTemplate.allowedUsers,
allowedHosts: certificateTemplate.allowedHosts,
allowUserCertificates: certificateTemplate.allowUserCertificates,
allowHostCertificates: certificateTemplate.allowHostCertificates,
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
}
}
});
return certificateTemplate;
}
});
server.route({
method: "DELETE",
url: "/:certificateTemplateId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
}),
response: {
200: sanitizedSshCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.sshCertificateTemplate.deleteSshCertTemplate({
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id
}
}
});
return certificateTemplate;
}
});
};

@ -36,7 +36,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
)
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
}),
response: {
@ -91,7 +91,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.optional()
.describe(PROJECT_ROLE.UPDATE.slug),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@ -86,13 +87,15 @@ export const accessApprovalPolicyServiceFactory = ({
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
project.id,
projectId: project.id,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@ -190,14 +193,14 @@ export const accessApprovalPolicyServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
// Anyone in the project should be able to get the policies.
/* const { permission } = */ await permissionService.getProjectPermission(
await permissionService.getProjectPermission({
actor,
actorId,
project.id,
projectId: project.id,
actorAuthMethod,
actorOrgId
);
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
return accessApprovalPolicies;
@ -241,13 +244,14 @@ export const accessApprovalPolicyServiceFactory = ({
if (!accessApprovalPolicy) {
throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
}
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
accessApprovalPolicy.projectId,
projectId: accessApprovalPolicy.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
@ -324,13 +328,14 @@ export const accessApprovalPolicyServiceFactory = ({
const policy = await accessApprovalPolicyDAL.findById(policyId);
if (!policy) throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
policy.projectId,
projectId: policy.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
@ -374,13 +379,14 @@ export const accessApprovalPolicyServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const { membership } = await permissionService.getProjectPermission(
const { membership } = await permissionService.getProjectPermission({
actor,
actorId,
project.id,
projectId: project.id,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
@ -413,13 +419,14 @@ export const accessApprovalPolicyServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
policy.projectId,
projectId: policy.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);

@ -1,7 +1,7 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@ -100,13 +100,14 @@ export const accessApprovalRequestServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
// Anyone can create an access approval request.
const { membership } = await permissionService.getProjectPermission(
const { membership } = await permissionService.getProjectPermission({
actor,
actorId,
project.id,
projectId: project.id,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
@ -213,7 +214,7 @@ export const accessApprovalRequestServiceFactory = ({
);
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`;
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
await triggerSlackNotification({
projectId: project.id,
@ -273,13 +274,14 @@ export const accessApprovalRequestServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const { membership } = await permissionService.getProjectPermission(
const { membership } = await permissionService.getProjectPermission({
actor,
actorId,
project.id,
projectId: project.id,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}
@ -318,13 +320,14 @@ export const accessApprovalRequestServiceFactory = ({
});
}
const { membership, hasRole } = await permissionService.getProjectPermission(
const { membership, hasRole } = await permissionService.getProjectPermission({
actor,
actorId,
accessApprovalRequest.projectId,
projectId: accessApprovalRequest.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
@ -422,13 +425,14 @@ export const accessApprovalRequestServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const { membership } = await permissionService.getProjectPermission(
const { membership } = await permissionService.getProjectPermission({
actor,
actorId,
project.id,
projectId: project.id,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (!membership) {
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
}

@ -93,7 +93,7 @@ export const auditLogStreamServiceFactory = ({
}
)
.catch((err) => {
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` });
});
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
const logStream = await auditLogStreamDAL.create({

@ -100,10 +100,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
// Filter by date range
if (startDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
}
if (endDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
}
// we timeout long running queries to prevent DB resource issues (2 minutes)

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
@ -26,13 +27,14 @@ export const auditLogServiceFactory = ({
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
// Filter logs for specific project
if (filter.projectId) {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
filter.projectId,
projectId: filter.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
} else {
// Organization-wide logs

@ -2,9 +2,14 @@ import {
TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
@ -26,7 +31,7 @@ export type TListProjectAuditLogDTO = {
export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
@ -95,6 +100,11 @@ export enum EventType {
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth",
ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth",
UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth",
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
@ -138,6 +148,17 @@ export enum EventType {
SECRET_APPROVAL_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
SIGN_SSH_KEY = "sign-ssh-key",
ISSUE_SSH_CREDS = "issue-ssh-creds",
CREATE_SSH_CA = "create-ssh-certificate-authority",
GET_SSH_CA = "get-ssh-certificate-authority",
UPDATE_SSH_CA = "update-ssh-certificate-authority",
DELETE_SSH_CA = "delete-ssh-certificate-authority",
GET_SSH_CA_CERTIFICATE_TEMPLATES = "get-ssh-certificate-authority-certificate-templates",
CREATE_SSH_CERTIFICATE_TEMPLATE = "create-ssh-certificate-template",
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
CREATE_CA = "create-certificate-authority",
GET_CA = "get-certificate-authority",
UPDATE_CA = "update-certificate-authority",
@ -203,7 +224,15 @@ export enum EventType {
CREATE_PROJECT_TEMPLATE = "create-project-template",
UPDATE_PROJECT_TEMPLATE = "update-project-template",
DELETE_PROJECT_TEMPLATE = "delete-project-template",
APPLY_PROJECT_TEMPLATE = "apply-project-template"
APPLY_PROJECT_TEMPLATE = "apply-project-template",
GET_APP_CONNECTIONS = "get-app-connections",
GET_APP_CONNECTION = "get-app-connection",
CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection",
CREATE_SHARED_SECRET = "create-shared-secret",
DELETE_SHARED_SECRET = "delete-shared-secret",
READ_SHARED_SECRET = "read-shared-secret"
}
interface UserActorMetadata {
@ -226,6 +255,8 @@ interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
interface UnknownUserActorMetadata {}
export interface UserActor {
type: ActorType.USER;
metadata: UserActorMetadata;
@ -241,6 +272,11 @@ export interface PlatformActor {
metadata: PlatformActorMetadata;
}
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
metadata: UnknownUserActorMetadata;
}
export interface IdentityActor {
type: ActorType.IDENTITY;
metadata: IdentityActorMetadata;
@ -903,6 +939,67 @@ interface GetIdentityOidcAuthEvent {
};
}
interface LoginIdentityJwtAuthEvent {
type: EventType.LOGIN_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
identityJwtAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityJwtAuthEvent {
type: EventType.ADD_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
configurationType: string;
jwksUrl?: string;
jwksCaCert: string;
publicKeys: string[];
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface UpdateIdentityJwtAuthEvent {
type: EventType.UPDATE_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
configurationType?: string;
jwksUrl?: string;
jwksCaCert?: string;
publicKeys?: string[];
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityJwtAuthEvent {
type: EventType.REVOKE_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
};
}
interface GetIdentityJwtAuthEvent {
type: EventType.GET_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
};
}
interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT;
metadata: {
@ -1140,6 +1237,117 @@ interface SecretApprovalRequest {
};
}
interface SignSshKey {
type: EventType.SIGN_SSH_KEY;
metadata: {
certificateTemplateId: string;
certType: SshCertType;
principals: string[];
ttl: string;
keyId: string;
};
}
interface IssueSshCreds {
type: EventType.ISSUE_SSH_CREDS;
metadata: {
certificateTemplateId: string;
keyAlgorithm: CertKeyAlgorithm;
certType: SshCertType;
principals: string[];
ttl: string;
keyId: string;
};
}
interface CreateSshCa {
type: EventType.CREATE_SSH_CA;
metadata: {
sshCaId: string;
friendlyName: string;
};
}
interface GetSshCa {
type: EventType.GET_SSH_CA;
metadata: {
sshCaId: string;
friendlyName: string;
};
}
interface UpdateSshCa {
type: EventType.UPDATE_SSH_CA;
metadata: {
sshCaId: string;
friendlyName: string;
status: SshCaStatus;
};
}
interface DeleteSshCa {
type: EventType.DELETE_SSH_CA;
metadata: {
sshCaId: string;
friendlyName: string;
};
}
interface GetSshCaCertificateTemplates {
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES;
metadata: {
sshCaId: string;
friendlyName: string;
};
}
interface CreateSshCertificateTemplate {
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
sshCaId: string;
name: string;
ttl: string;
maxTTL: string;
allowedUsers: string[];
allowedHosts: string[];
allowUserCertificates: boolean;
allowHostCertificates: boolean;
allowCustomKeyIds: boolean;
};
}
interface GetSshCertificateTemplate {
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface UpdateSshCertificateTemplate {
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
sshCaId: string;
name: string;
status: SshCertTemplateStatus;
ttl: string;
maxTTL: string;
allowedUsers: string[];
allowedHosts: string[];
allowUserCertificates: boolean;
allowHostCertificates: boolean;
allowCustomKeyIds: boolean;
};
}
interface DeleteSshCertificateTemplate {
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface CreateCa {
type: EventType.CREATE_CA;
metadata: {
@ -1676,6 +1884,68 @@ interface ApplyProjectTemplateEvent {
};
}
interface GetAppConnectionsEvent {
type: EventType.GET_APP_CONNECTIONS;
metadata: {
app?: AppConnection;
count: number;
connectionIds: string[];
};
}
interface GetAppConnectionEvent {
type: EventType.GET_APP_CONNECTION;
metadata: {
connectionId: string;
};
}
interface CreateAppConnectionEvent {
type: EventType.CREATE_APP_CONNECTION;
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
}
interface UpdateAppConnectionEvent {
type: EventType.UPDATE_APP_CONNECTION;
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
}
interface DeleteAppConnectionEvent {
type: EventType.DELETE_APP_CONNECTION;
metadata: {
connectionId: string;
};
}
interface CreateSharedSecretEvent {
type: EventType.CREATE_SHARED_SECRET;
metadata: {
id: string;
accessType: string;
name?: string;
expiresAfterViews?: number;
usingPassword: boolean;
expiresAt: string;
};
}
interface DeleteSharedSecretEvent {
type: EventType.DELETE_SHARED_SECRET;
metadata: {
id: string;
name?: string;
};
}
interface ReadSharedSecretEvent {
type: EventType.READ_SHARED_SECRET;
metadata: {
id: string;
name?: string;
accessType: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -1742,6 +2012,11 @@ export type Event =
| DeleteIdentityOidcAuthEvent
| UpdateIdentityOidcAuthEvent
| GetIdentityOidcAuthEvent
| LoginIdentityJwtAuthEvent
| AddIdentityJwtAuthEvent
| UpdateIdentityJwtAuthEvent
| GetIdentityJwtAuthEvent
| DeleteIdentityJwtAuthEvent
| CreateEnvironmentEvent
| GetEnvironmentEvent
| UpdateEnvironmentEvent
@ -1766,6 +2041,17 @@ export type Event =
| SecretApprovalClosed
| SecretApprovalRequest
| SecretApprovalReopened
| SignSshKey
| IssueSshCreds
| CreateSshCa
| GetSshCa
| UpdateSshCa
| DeleteSshCa
| GetSshCaCertificateTemplates
| CreateSshCertificateTemplate
| UpdateSshCertificateTemplate
| GetSshCertificateTemplate
| DeleteSshCertificateTemplate
| CreateCa
| GetCa
| UpdateCa
@ -1831,4 +2117,12 @@ export type Event =
| CreateProjectTemplateEvent
| UpdateProjectTemplateEvent
| DeleteProjectTemplateEvent
| ApplyProjectTemplateEvent;
| ApplyProjectTemplateEvent
| GetAppConnectionsEvent
| GetAppConnectionEvent
| CreateAppConnectionEvent
| UpdateAppConnectionEvent
| DeleteAppConnectionEvent
| CreateSharedSecretEvent
| DeleteSharedSecretEvent
| ReadSharedSecretEvent;

@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -66,13 +67,14 @@ export const certificateAuthorityCrlServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
ca.projectId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms";
import { SecretKeyEncoding } from "@app/db/schemas";
import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
@ -67,13 +67,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -146,13 +147,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -225,13 +227,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -294,13 +297,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -336,13 +340,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
@ -73,13 +73,14 @@ export const dynamicSecretServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.CreateRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -144,13 +145,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -227,13 +229,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -287,13 +290,14 @@ export const dynamicSecretServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -337,13 +341,14 @@ export const dynamicSecretServiceFactory = ({
isInternal
}: TListDynamicSecretsMultiEnvDTO) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
@ -380,13 +385,14 @@ export const dynamicSecretServiceFactory = ({
search,
projectId
}: TGetDynamicSecretsCountDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -428,13 +434,14 @@ export const dynamicSecretServiceFactory = ({
projectId = project.id;
}
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -459,13 +466,14 @@ export const dynamicSecretServiceFactory = ({
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actor.authMethod,
actor.orgId
);
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager
});
const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) =>
permission.can(
@ -504,13 +512,14 @@ export const dynamicSecretServiceFactory = ({
...params
}: TListDynamicSecretsMultiEnvDTO) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>

@ -34,6 +34,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
const db = knex({
client: providerInputs.client,
connection: {
@ -43,7 +45,16 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
user: providerInputs.username,
password: providerInputs.password,
ssl,
pool: { min: 0, max: 1 }
pool: { min: 0, max: 1 },
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
options: isMsSQLClient
? {
trustServerCertificate: !providerInputs.ca,
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
}
: undefined
},
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
});

@ -1,7 +1,9 @@
import { KMSServiceException } from "@aws-sdk/client-kms";
import { STSServiceException } from "@aws-sdk/client-sts";
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@ -71,7 +73,16 @@ export const externalKmsServiceFactory = ({
switch (provider.type) {
case KmsProviders.Aws:
{
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }).catch((error) => {
if (error instanceof STSServiceException || error instanceof KMSServiceException) {
throw new InternalServerError({
message: error.message ? `AWS error: ${error.message}` : ""
});
}
throw error;
});
// if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput);

@ -5,6 +5,8 @@ import { TableName, TGroups } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
import { EFilterReturnedUsers } from "./group-types";
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
export const groupDALFactory = (db: TDbClient) => {
@ -66,7 +68,8 @@ export const groupDALFactory = (db: TDbClient) => {
offset = 0,
limit,
username, // depreciated in favor of search
search
search,
filter
}: {
orgId: string;
groupId: string;
@ -74,6 +77,7 @@ export const groupDALFactory = (db: TDbClient) => {
limit?: number;
username?: string;
search?: string;
filter?: EFilterReturnedUsers;
}) => {
try {
const query = db
@ -90,6 +94,7 @@ export const groupDALFactory = (db: TDbClient) => {
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("createdAt").withSchema(TableName.UserGroupMembership).as("joinedGroupAt"),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
@ -111,17 +116,37 @@ export const groupDALFactory = (db: TDbClient) => {
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
}
switch (filter) {
case EFilterReturnedUsers.EXISTING_MEMBERS:
void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is not", null);
break;
case EFilterReturnedUsers.NON_MEMBERS:
void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is", null);
break;
default:
break;
}
const members = await query;
return {
members: members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
({
email,
username: memberUsername,
firstName,
lastName,
userId,
groupId: memberGroupId,
joinedGroupAt
}) => ({
id: userId,
email,
username: memberUsername,
firstName,
lastName,
isPartOfGroup: !!memberGroupId
isPartOfGroup: !!memberGroupId,
joinedGroupAt
})
),
// @ts-expect-error col select is raw and not strongly typed

@ -32,7 +32,7 @@ type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById"
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
@ -88,12 +88,26 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
const group = await groupDAL.create({
name,
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
orgId: actorOrgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
const group = await groupDAL.transaction(async (tx) => {
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
if (existingGroup) {
throw new BadRequestError({
message: `Failed to create group with name '${name}'. Group with the same name already exists`
});
}
const newGroup = await groupDAL.create(
{
name,
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
orgId: actorOrgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
},
tx
);
return newGroup;
});
return group;
@ -145,21 +159,36 @@ export const groupServiceFactory = ({
if (isCustomRole) customRole = customOrgRole;
}
const [updatedGroup] = await groupDAL.update(
{
id: group.id
},
{
name,
slug: slug ? slugify(slug) : undefined,
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
const updatedGroup = await groupDAL.transaction(async (tx) => {
if (name) {
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
if (existingGroup && existingGroup.id !== id) {
throw new BadRequestError({
message: `Failed to update group with name '${name}'. Group with the same name already exists`
});
}
}
);
const [updated] = await groupDAL.update(
{
id: group.id
},
{
name,
slug: slug ? slugify(slug) : undefined,
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
},
tx
);
return updated;
});
return updatedGroup;
};
@ -222,7 +251,8 @@ export const groupServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
search
search,
filter
}: TListGroupUsersDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
@ -251,7 +281,8 @@ export const groupServiceFactory = ({
offset,
limit,
username,
search
search,
filter
});
return { users: members, totalCount };
@ -283,8 +314,8 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges)
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
const user = await userDAL.findOne({ username });
@ -338,8 +369,8 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges)
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const user = await userDAL.findOne({ username });

@ -39,6 +39,7 @@ export type TListGroupUsersDTO = {
limit: number;
username?: string;
search?: string;
filter?: EFilterReturnedUsers;
} & TGenericPermission;
export type TAddUserToGroupDTO = {
@ -101,3 +102,8 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
export enum EFilterReturnedUsers {
EXISTING_MEMBERS = "existingMembers",
NON_MEMBERS = "nonMembers"
}

@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import ms from "ms";
import { TableName } from "@app/db/schemas";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
@ -55,24 +55,26 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityId,
identityProjectMembership.projectId,
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
@ -135,24 +137,26 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
@ -215,24 +219,26 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
@ -260,13 +266,14 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
@ -294,13 +301,14 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
@ -329,13 +337,14 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })

@ -2,6 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
@ -62,25 +63,27 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityId,
identityProjectMembership.projectId,
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
@ -143,26 +146,28 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
@ -242,25 +247,27 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
@ -299,13 +306,14 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
@ -341,13 +349,14 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
identityProjectMembership.projectId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,

@ -476,14 +476,14 @@ export const ldapConfigServiceFactory = ({
});
} else {
const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."

@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
rbac: false,
customRateLimits: false,
customAlerts: false,
secretAccessInsights: false,
auditLogs: false,
auditLogsRetentionDays: 0,
auditLogStreams: false,
@ -49,7 +50,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
},
pkiEst: false,
enforceMfa: false,
projectTemplates: false
projectTemplates: false,
appConnections: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

@ -246,8 +246,7 @@ export const licenseServiceFactory = ({
};
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const plan = await getPlan(orgId, projectId);
return plan;
};

@ -48,6 +48,7 @@ export type TFeatureSet = {
samlSSO: false;
hsm: false;
oidcSSO: false;
secretAccessInsights: false;
scim: false;
ldap: false;
groups: false;
@ -67,6 +68,7 @@ export type TFeatureSet = {
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: false;
appConnections: false; // TODO: remove once live
};
export type TOrgPlansTableDTO = {

@ -27,7 +27,8 @@ export enum OrgPermissionSubjects {
Kms = "kms",
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates"
ProjectTemplates = "project-templates",
AppConnections = "app-connections"
}
export type OrgPermissionSet =
@ -46,6 +47,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => {
@ -123,6 +125,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return rules;
@ -153,6 +160,8 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
return rules;
};

@ -125,6 +125,404 @@ export const permissionDALFactory = (db: TDbClient) => {
}
};
const getProjectGroupPermissions = async (projectId: string) => {
try {
const docs = await db
.replicaNode()(TableName.GroupProjectMembership)
.join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin<TProjectRoles>(
{ groupCustomRoles: TableName.ProjectRoles },
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`groupCustomRoles.id`
)
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.Groups).as("groupId"),
db.ref("name").withSchema(TableName.Groups).as("groupName"),
db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"),
db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"),
db
.ref("customRoleId")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleCustomRoleId"),
db
.ref("isTemporary")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleIsTemporary"),
db
.ref("temporaryMode")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryMode"),
db
.ref("temporaryRange")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryAccessEndTime")
);
const groupPermissions = sqlNestRelationships({
data: docs,
key: "groupId",
parentMapper: ({ groupId, groupName, membershipId }) => ({
groupId,
username: groupName,
id: membershipId
}),
childrenMapper: [
{
key: "groupProjectMembershipRoleId",
label: "groupRoles" as const,
mapper: ({
groupProjectMembershipRoleId,
groupProjectMembershipRole,
groupProjectMembershipRolePermission,
groupProjectMembershipRoleCustomRoleSlug,
groupProjectMembershipRoleIsTemporary,
groupProjectMembershipRoleTemporaryMode,
groupProjectMembershipRoleTemporaryAccessEndTime,
groupProjectMembershipRoleTemporaryAccessStartTime,
groupProjectMembershipRoleTemporaryRange
}) => ({
id: groupProjectMembershipRoleId,
role: groupProjectMembershipRole,
customRoleSlug: groupProjectMembershipRoleCustomRoleSlug,
permissions: groupProjectMembershipRolePermission,
temporaryRange: groupProjectMembershipRoleTemporaryRange,
temporaryMode: groupProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: groupProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: groupProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: groupProjectMembershipRoleIsTemporary
})
}
]
});
return groupPermissions
.map((groupPermission) => {
if (!groupPermission) return undefined;
const activeGroupRoles =
groupPermission?.groupRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
return {
...groupPermission,
roles: activeGroupRoles
};
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectGroupPermissions" });
}
};
const getProjectUserPermissions = async (projectId: string) => {
try {
const docs = await db
.replicaNode()(TableName.Users)
.where("isGhost", "=", false)
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]));
})
.leftJoin(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin<TProjectRoles>(
{ groupCustomRoles: TableName.ProjectRoles },
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`groupCustomRoles.id`
)
.join(TableName.ProjectMembership, (queryBuilder) => {
void queryBuilder
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
})
.leftJoin(
TableName.ProjectUserMembershipRole,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
void queryBuilder
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
})
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
})
.select(
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("username").withSchema(TableName.Users).as("username"),
// groups specific
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
db
.ref("customRoleId")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleCustomRoleId"),
db
.ref("isTemporary")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleIsTemporary"),
db
.ref("temporaryMode")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryMode"),
db
.ref("temporaryRange")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
// user specific
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
db
.ref("temporaryMode")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryMode"),
db
.ref("isTemporary")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
db
.ref("permissions")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesPermissions"),
db
.ref("temporaryMode")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryMode"),
db
.ref("isTemporary")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryRange"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
// general
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
const userPermissions = sqlNestRelationships({
data: docs,
key: "userId",
parentMapper: ({
orgId,
username,
orgAuthEnforced,
membershipId,
groupMembershipId,
membershipCreatedAt,
groupMembershipCreatedAt,
groupMembershipUpdatedAt,
membershipUpdatedAt,
projectType,
userId
}) => ({
orgId,
orgAuthEnforced,
userId,
projectId,
username,
projectType,
id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
}),
childrenMapper: [
{
key: "userGroupProjectMembershipRoleId",
label: "userGroupRoles" as const,
mapper: ({
userGroupProjectMembershipRoleId,
userGroupProjectMembershipRole,
userGroupProjectMembershipRolePermission,
userGroupProjectMembershipRoleCustomRoleSlug,
userGroupProjectMembershipRoleIsTemporary,
userGroupProjectMembershipRoleTemporaryMode,
userGroupProjectMembershipRoleTemporaryAccessEndTime,
userGroupProjectMembershipRoleTemporaryAccessStartTime,
userGroupProjectMembershipRoleTemporaryRange
}) => ({
id: userGroupProjectMembershipRoleId,
role: userGroupProjectMembershipRole,
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
permissions: userGroupProjectMembershipRolePermission,
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: userGroupProjectMembershipRoleIsTemporary
})
},
{
key: "userProjectMembershipRoleId",
label: "projectMembershipRoles" as const,
mapper: ({
userProjectMembershipRoleId,
userProjectMembershipRole,
userProjectCustomRolePermission,
userProjectMembershipRoleIsTemporary,
userProjectMembershipRoleTemporaryMode,
userProjectMembershipRoleTemporaryRange,
userProjectMembershipRoleTemporaryAccessEndTime,
userProjectMembershipRoleTemporaryAccessStartTime,
userProjectMembershipRoleCustomRoleSlug
}) => ({
id: userProjectMembershipRoleId,
role: userProjectMembershipRole,
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
permissions: userProjectCustomRolePermission,
temporaryRange: userProjectMembershipRoleTemporaryRange,
temporaryMode: userProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: userProjectMembershipRoleIsTemporary
})
},
{
key: "userAdditionalPrivilegesId",
label: "additionalPrivileges" as const,
mapper: ({
userAdditionalPrivilegesId,
userAdditionalPrivilegesPermissions,
userAdditionalPrivilegesIsTemporary,
userAdditionalPrivilegesTemporaryMode,
userAdditionalPrivilegesTemporaryRange,
userAdditionalPrivilegesTemporaryAccessEndTime,
userAdditionalPrivilegesTemporaryAccessStartTime
}) => ({
id: userAdditionalPrivilegesId,
permissions: userAdditionalPrivilegesPermissions,
temporaryRange: userAdditionalPrivilegesTemporaryRange,
temporaryMode: userAdditionalPrivilegesTemporaryMode,
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
isTemporary: userAdditionalPrivilegesIsTemporary
})
},
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return userPermissions
.map((userPermission) => {
if (!userPermission) return undefined;
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined;
// when introducting cron mode change it here
const activeRoles =
userPermission?.projectMembershipRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeGroupRoles =
userPermission?.userGroupRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeAdditionalPrivileges =
userPermission?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
return {
...userPermission,
roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: activeAdditionalPrivileges
};
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectUserPermissions" });
}
};
const getProjectPermission = async (userId: string, projectId: string) => {
try {
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
@ -269,6 +667,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
@ -284,13 +683,15 @@ export const permissionDALFactory = (db: TDbClient) => {
membershipCreatedAt,
groupMembershipCreatedAt,
groupMembershipUpdatedAt,
membershipUpdatedAt
membershipUpdatedAt,
projectType
}) => ({
orgId,
orgAuthEnforced,
userId,
projectId,
username,
projectType,
id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
@ -411,6 +812,163 @@ export const permissionDALFactory = (db: TDbClient) => {
}
};
const getProjectIdentityPermissions = async (projectId: string) => {
try {
const docs = await db
.replicaNode()(TableName.IdentityProjectMembership)
.join(
TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
.leftJoin(
TableName.ProjectRoles,
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.leftJoin(
TableName.IdentityProjectAdditionalPrivilege,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.join(
// Join the Project table to later select orgId
TableName.Project,
`${TableName.IdentityProjectMembership}.projectId`,
`${TableName.Project}.id`
)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
})
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.Identity).as("identityId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles),
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
db
.ref("temporaryMode")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryAccessEndTime"),
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
);
const permissions = sqlNestRelationships({
data: docs,
key: "identityId",
parentMapper: ({
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
orgId,
identityName,
projectType,
identityId
}) => ({
id: membershipId,
identityId,
username: identityName,
projectId,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
orgId,
projectType,
// just a prefilled value
orgAuthEnforced: false
}),
childrenMapper: [
{
key: "id",
label: "roles" as const,
mapper: (data) =>
IdentityProjectMembershipRoleSchema.extend({
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
},
{
key: "identityApId",
label: "additionalPrivileges" as const,
mapper: ({
identityApId,
identityApPermissions,
identityApIsTemporary,
identityApTemporaryMode,
identityApTemporaryRange,
identityApTemporaryAccessEndTime,
identityApTemporaryAccessStartTime
}) => ({
id: identityApId,
permissions: identityApPermissions,
temporaryRange: identityApTemporaryRange,
temporaryMode: identityApTemporaryMode,
temporaryAccessEndTime: identityApTemporaryAccessEndTime,
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
isTemporary: identityApIsTemporary
})
},
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return permissions
.map((permission) => {
if (!permission) {
return undefined;
}
// when introducting cron mode change it here
const activeRoles = permission?.roles.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
const activeAdditionalPrivileges = permission?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
return { ...permission, roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectIdentityPermissions" });
}
};
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
try {
const docs = await db
@ -449,6 +1007,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
@ -480,7 +1039,14 @@ export const permissionDALFactory = (db: TDbClient) => {
const permission = sqlNestRelationships({
data: docs,
key: "membershipId",
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
parentMapper: ({
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
orgId,
identityName,
projectType
}) => ({
id: membershipId,
identityId,
username: identityName,
@ -488,6 +1054,7 @@ export const permissionDALFactory = (db: TDbClient) => {
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
orgId,
projectType,
// just a prefilled value
orgAuthEnforced: false
}),
@ -556,6 +1123,9 @@ export const permissionDALFactory = (db: TDbClient) => {
getOrgPermission,
getOrgIdentityPermission,
getProjectPermission,
getProjectIdentityPermission
getProjectIdentityPermission,
getProjectUserPermissions,
getProjectIdentityPermissions,
getProjectGroupPermissions
};
};

@ -1,3 +1,6 @@
import { ActionProjectType } from "@app/db/schemas";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export type TBuildProjectPermissionDTO = {
permissions?: unknown;
role: string;
@ -7,3 +10,34 @@ export type TBuildOrgPermissionDTO = {
permissions?: unknown;
role: string;
}[];
export type TGetUserProjectPermissionArg = {
userId: string;
projectId: string;
authMethod: ActorAuthMethod;
actionProjectType: ActionProjectType;
userOrgId?: string;
};
export type TGetIdentityProjectPermissionArg = {
identityId: string;
projectId: string;
identityOrgId?: string;
actionProjectType: ActionProjectType;
};
export type TGetServiceTokenProjectPermissionArg = {
serviceTokenId: string;
projectId: string;
actorOrgId?: string;
actionProjectType: ActionProjectType;
};
export type TGetProjectPermissionArg = {
actor: ActorType;
actorId: string;
projectId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId?: string;
actionProjectType: ActionProjectType;
};

@ -4,6 +4,7 @@ import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
import {
ActionProjectType,
OrgMembershipRole,
ProjectMembershipRole,
ServiceTokenScopes,
@ -22,7 +23,14 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
import {
TBuildOrgPermissionDTO,
TBuildProjectPermissionDTO,
TGetIdentityProjectPermissionArg,
TGetProjectPermissionArg,
TGetServiceTokenProjectPermissionArg,
TGetUserProjectPermissionArg
} from "./permission-service-types";
import {
buildServiceTokenProjectPermission,
projectAdminPermissions,
@ -192,12 +200,13 @@ export const permissionServiceFactory = ({
};
// user permission for a project in an organization
const getUserProjectPermission = async (
userId: string,
projectId: string,
authMethod: ActorAuthMethod,
userOrgId?: string
): Promise<TProjectPermissionRT<ActorType.USER>> => {
const getUserProjectPermission = async ({
userId,
projectId,
authMethod,
userOrgId,
actionProjectType
}: TGetUserProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.USER>> => {
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" });
@ -218,6 +227,12 @@ export const permissionServiceFactory = ({
validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced);
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.`
});
}
// join two permissions and pass to build the final permission set
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
@ -262,11 +277,12 @@ export const permissionServiceFactory = ({
};
};
const getIdentityProjectPermission = async (
identityId: string,
projectId: string,
identityOrgId: string | undefined
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
const getIdentityProjectPermission = async ({
identityId,
projectId,
identityOrgId,
actionProjectType
}: TGetIdentityProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!identityProjectPermission)
throw new ForbiddenRequestError({
@ -285,6 +301,12 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" });
}
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.`
});
}
const rolePermissions =
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
@ -330,11 +352,12 @@ export const permissionServiceFactory = ({
};
};
const getServiceTokenProjectPermission = async (
serviceTokenId: string,
projectId: string,
actorOrgId: string | undefined
) => {
const getServiceTokenProjectPermission = async ({
serviceTokenId,
projectId,
actorOrgId,
actionProjectType
}: TGetServiceTokenProjectPermissionArg) => {
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${serviceTokenId}' not found` });
@ -358,6 +381,12 @@ export const permissionServiceFactory = ({
});
}
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== serviceTokenProject.type) {
throw new BadRequestError({
message: `The project is of type ${serviceTokenProject.type}. Operations of type ${actionProjectType} are not allowed.`
});
}
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
@ -381,20 +410,154 @@ export const permissionServiceFactory = ({
hasRole: (role: string) => boolean;
};
const getProjectPermission = async <T extends ActorType>(
type: T,
id: string,
projectId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
): Promise<TProjectPermissionRT<T>> => {
switch (type) {
const getProjectPermissions = async (projectId: string) => {
// fetch user permissions
const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId);
const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => {
const rolePermissions =
userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
role: ProjectMembershipRole.Custom,
permissions
})) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
);
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return {
permission,
id: userProjectPermission.userId,
name: userProjectPermission.username,
membershipId: userProjectPermission.id
};
});
// fetch identity permissions
const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId);
const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => {
const rolePermissions =
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
role: ProjectMembershipRole.Custom,
permissions
})) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
);
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return {
permission,
id: identityProjectPermission.identityId,
name: identityProjectPermission.username,
membershipId: identityProjectPermission.id
};
});
// fetch group permissions
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId);
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {
const rolePermissions =
groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const rules = buildProjectPermissionRules(rolePermissions);
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
return {
permission,
id: groupProjectPermission.groupId,
name: groupProjectPermission.username,
membershipId: groupProjectPermission.id
};
});
return {
userPermissions,
identityPermissions,
groupPermissions
};
};
const getProjectPermission = async <T extends ActorType>({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
switch (actor) {
case ActorType.USER:
return getUserProjectPermission(id, projectId, actorAuthMethod, actorOrgId) as Promise<TProjectPermissionRT<T>>;
return getUserProjectPermission({
userId: actorId,
projectId,
authMethod: actorAuthMethod,
userOrgId: actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
case ActorType.SERVICE:
return getServiceTokenProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
return getServiceTokenProjectPermission({
serviceTokenId: actorId,
projectId,
actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
case ActorType.IDENTITY:
return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
return getIdentityProjectPermission({
identityId: actorId,
projectId,
identityOrgId: actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
default:
throw new BadRequestError({
message: "Invalid actor provided",
@ -431,6 +594,7 @@ export const permissionServiceFactory = ({
getOrgPermission,
getUserProjectPermission,
getProjectPermission,
getProjectPermissions,
getOrgPermissionByRole,
getProjectPermissionByRole,
buildOrgPermission,

@ -54,6 +54,9 @@ export enum ProjectPermissionSub {
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates",
CertificateTemplates = "certificate-templates",
SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificates = "ssh-certificates",
SshCertificateTemplates = "ssh-certificate-templates",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms",
@ -132,6 +135,9 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
@ -338,6 +344,28 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take."
)
}),
z.object({
subject: z
.literal(ProjectPermissionSub.SshCertificateAuthorities)
.describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SshCertificates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z
.literal(ProjectPermissionSub.SshCertificateTemplates)
.describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
@ -480,7 +508,10 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Certificates,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates
].forEach((el) => {
can(
[
@ -665,6 +696,11 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can(
[
ProjectPermissionCmekActions.Create,
@ -707,6 +743,9 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
return rules;
};

@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { TableName } from "@app/db/schemas";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
@ -55,21 +55,23 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectMembership.projectId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission(
ActorType.USER,
projectMembership.userId,
projectMembership.projectId,
const { permission: targetUserPermission } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: projectMembership.userId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
@ -140,21 +142,23 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
});
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectMembership.projectId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission(
ActorType.USER,
projectMembership.userId,
projectMembership.projectId,
const { permission: targetUserPermission } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: projectMembership.userId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
@ -224,13 +228,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
});
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectMembership.projectId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
@ -260,13 +265,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
});
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectMembership.projectId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return {
@ -286,13 +292,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectMembership.projectId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find(

@ -421,14 +421,14 @@ export const samlConfigServiceFactory = ({
});
} else {
const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."

@ -6,7 +6,8 @@ export enum SamlProviders {
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml"
KEYCLOAK_SAML = "keycloak-saml",
AUTH0_SAML = "auth0-saml"
}
export type TCreateSamlCfgDTO = {

@ -531,7 +531,7 @@ export const scimServiceFactory = ({
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : true
isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
},
tx
);
@ -790,6 +790,21 @@ export const scimServiceFactory = ({
});
const newGroup = await groupDAL.transaction(async (tx) => {
const conflictingGroup = await groupDAL.findOne(
{
name: displayName,
orgId
},
tx
);
if (conflictingGroup) {
throw new ScimRequestError({
detail: `Group with name '${displayName}' already exists in the organization`,
status: 409
});
}
const group = await groupDAL.create(
{
name: displayName,

@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import picomatch from "picomatch";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -78,13 +79,14 @@ export const secretApprovalPolicyServiceFactory = ({
if (!groupApprovers.length && approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@ -191,13 +193,14 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
secretApprovalPolicy.projectId,
projectId: secretApprovalPolicy.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
const plan = await licenseService.getPlan(actorOrgId);
@ -285,13 +288,14 @@ export const secretApprovalPolicyServiceFactory = ({
if (!sapPolicy)
throw new NotFoundError({ message: `Secret approval policy with ID '${secretPolicyId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
sapPolicy.projectId,
projectId: sapPolicy.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
@ -324,13 +328,14 @@ export const secretApprovalPolicyServiceFactory = ({
actorAuthMethod,
projectId
}: TListSapDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null });
@ -368,7 +373,14 @@ export const secretApprovalPolicyServiceFactory = ({
environment,
secretPath
}: TGetBoardSapDTO) => {
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
return getSecretApprovalPolicy(projectId, environment, secretPath);
};
@ -388,13 +400,14 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
sapPolicy.projectId,
projectId: sapPolicy.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);

@ -36,7 +36,7 @@ export const sendApprovalEmailsFn = async ({
firstName: reviewerUser.firstName,
projectName: project.name,
organizationName: project.organization.name,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}`
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval?requestId=${secretApprovalRequest.id}`
},
template: SmtpTemplates.SecretApprovalRequestNeedsReview
});

@ -256,6 +256,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
db.ref("id").withSchema("secVerTag")
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({
secVerTagId: "secVerTag.id",
@ -279,6 +280,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
)
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
const formatedDoc = sqlNestRelationships({
data: doc,
@ -338,9 +344,19 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
})
}
]
},
{
key: "metadataId",
label: "oldSecretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el,
secret: secret?.[0],

@ -1,6 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import {
ActionProjectType,
ProjectMembershipRole,
SecretEncryptionAlgo,
SecretKeyEncoding,
@ -21,6 +22,8 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import {
decryptSecretWithBot,
@ -90,6 +93,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
@ -137,18 +141,20 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
licenseService,
projectSlackConfigDAL
projectSlackConfigDAL,
resourceMetadataDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
await permissionService.getProjectPermission(
actor as ActorType.USER,
await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
return count;
@ -168,7 +174,14 @@ export const secretApprovalRequestServiceFactory = ({
}: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
@ -211,13 +224,14 @@ export const secretApprovalRequestServiceFactory = ({
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerUserId !== actorId &&
@ -240,6 +254,7 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key,
id: el.id,
version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
@ -268,7 +283,8 @@ export const secretApprovalRequestServiceFactory = ({
secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: "",
tags: el.secretVersion.tags
tags: el.secretVersion.tags,
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO
}
: undefined
}));
@ -329,13 +345,14 @@ export const secretApprovalRequestServiceFactory = ({
});
}
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
const { hasRole } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId,
secretApprovalRequest.projectId,
projectId: secretApprovalRequest.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerUserId !== actorId &&
@ -395,13 +412,14 @@ export const secretApprovalRequestServiceFactory = ({
});
}
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
const { hasRole } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId,
secretApprovalRequest.projectId,
projectId: secretApprovalRequest.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerUserId !== actorId &&
@ -451,13 +469,14 @@ export const secretApprovalRequestServiceFactory = ({
});
}
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
const { hasRole } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
if (
!hasRole(ProjectMembershipRole.Admin) &&
@ -542,6 +561,7 @@ export const secretApprovalRequestServiceFactory = ({
? await fnSecretV2BridgeBulkInsert({
tx,
folderId,
orgId: actorOrgId,
inputSecrets: secretCreationCommits.map((el) => ({
tagIds: el?.tags.map(({ id }) => id),
version: 1,
@ -549,6 +569,7 @@ export const secretApprovalRequestServiceFactory = ({
encryptedValue: el.encryptedValue,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
references: el.encryptedValue
? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({
@ -558,6 +579,7 @@ export const secretApprovalRequestServiceFactory = ({
: [],
type: SecretType.Shared
})),
resourceMetadataDAL,
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
@ -567,6 +589,7 @@ export const secretApprovalRequestServiceFactory = ({
const updatedSecrets = secretUpdationCommits.length
? await fnSecretV2BridgeBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
@ -591,6 +614,7 @@ export const secretApprovalRequestServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
tags: el?.tags.map(({ id }) => id),
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
...encryptedValue
}
};
@ -598,7 +622,8 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
resourceMetadataDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
@ -823,6 +848,7 @@ export const secretApprovalRequestServiceFactory = ({
}
await secretQueueService.syncSecrets({
projectId,
orgId: actorOrgId,
secretPath: folder.path,
environmentSlug: folder.environmentSlug,
actorId,
@ -851,7 +877,7 @@ export const secretApprovalRequestServiceFactory = ({
bypassReason,
secretPath: policy.secretPath,
environment: env.name,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`
},
template: SmtpTemplates.AccessSecretRequestBypassed
});
@ -875,13 +901,14 @@ export const secretApprovalRequestServiceFactory = ({
}: TGenerateSecretApprovalRequestDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@ -1155,14 +1182,14 @@ export const secretApprovalRequestServiceFactory = ({
if (actor === ActorType.SERVICE || actor === ActorType.Machine)
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new NotFoundError({
@ -1206,6 +1233,7 @@ export const secretApprovalRequestServiceFactory = ({
),
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
key: createdSecret.secretKey,
secretMetadata: createdSecret.secretMetadata,
type: SecretType.Shared
}))
);
@ -1239,9 +1267,10 @@ export const secretApprovalRequestServiceFactory = ({
type: SecretType.Shared
}))
);
if (secrets.length)
if (secrets.length !== secretsWithNewName.length)
throw new NotFoundError({
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
message: `Secret does not exist: ${secrets.map((el) => el.key).join(",")}`
});
}
@ -1261,12 +1290,14 @@ export const secretApprovalRequestServiceFactory = ({
reminderNote,
secretComment,
metadata,
skipMultilineEncoding
skipMultilineEncoding,
secretMetadata
}) => {
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
return {
...latestSecretVersions[secretId],
secretMetadata,
key: newSecretName || secretKey,
encryptedComment: setKnexStringValue(
secretComment,
@ -1368,7 +1399,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays,
encryptedValue,
secretId,
secretVersion
secretVersion,
secretMetadata
}) => ({
version,
requestId: doc.id,
@ -1381,7 +1413,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays,
reminderNote,
encryptedComment,
key
key,
secretMetadata: JSON.stringify(secretMetadata)
})
),
tx

@ -1,5 +1,6 @@
import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations } from "@app/services/secret/secret-types";
export enum RequestState {
@ -34,6 +35,7 @@ export type TApprovalCreateSecretV2Bridge = {
reminderRepeatDays?: number | null;
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
secretMetadata?: ResourceMetadataDTO;
tagIds?: string[];
};

@ -13,6 +13,8 @@ import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
@ -56,6 +58,7 @@ type TSecretReplicationServiceFactoryDep = {
>;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
@ -121,7 +124,8 @@ export const secretReplicationServiceFactory = ({
secretVersionV2TagBridgeDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService
kmsService,
resourceMetadataDAL
}: TSecretReplicationServiceFactoryDep) => {
const $getReplicatedSecrets = (
botKey: string,
@ -151,8 +155,10 @@ export const secretReplicationServiceFactory = ({
};
const $getReplicatedSecretsV2 = (
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[],
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[]
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[],
importedSecrets: {
secrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[];
}[]
) => {
const deDupe = new Set<string>();
const secrets = [...localSecrets];
@ -178,6 +184,7 @@ export const secretReplicationServiceFactory = ({
secretPath,
environmentSlug,
projectId,
orgId,
actorId,
actor,
pickOnlyImportIds,
@ -222,6 +229,7 @@ export const secretReplicationServiceFactory = ({
.map(({ folderId }) =>
secretQueueService.replicateSecrets({
projectId,
orgId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
actorId,
@ -267,6 +275,7 @@ export const secretReplicationServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined
}));
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
@ -333,13 +342,29 @@ export const secretReplicationServiceFactory = ({
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
const locallyUpdatedSecrets = sourceSecrets
.filter(
({ key, secretKey, secretValue }) =>
.filter(({ key, secretKey, secretValue, secretMetadata }) => {
const sourceSecretMetadataJson = JSON.stringify(
(secretMetadata ?? []).map((entry) => ({
key: entry.key,
value: entry.value
}))
);
const destinationSecretMetadataJson = JSON.stringify(
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretMetadata ?? []).map((entry) => ({
key: entry.key,
value: entry.value
}))
);
return (
destinationLocalSecretsGroupedByKey[key]?.[0] &&
// if key or value changed
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue)
)
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue ||
sourceSecretMetadataJson !== destinationSecretMetadataJson)
);
})
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
const locallyDeletedSecrets = destinationLocalSecrets
@ -387,6 +412,7 @@ export const secretReplicationServiceFactory = ({
op: operation,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
secretMetadata: JSON.stringify(doc.secretMetadata),
key: doc.key,
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
@ -406,10 +432,12 @@ export const secretReplicationServiceFactory = ({
if (locallyCreatedSecrets.length) {
await fnSecretV2BridgeBulkInsert({
folderId: destinationReplicationFolderId,
orgId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
secretTagDAL,
resourceMetadataDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
@ -419,6 +447,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
};
})
@ -426,10 +455,12 @@ export const secretReplicationServiceFactory = ({
}
if (locallyUpdatedSecrets.length) {
await fnSecretV2BridgeBulkUpdate({
orgId,
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
resourceMetadataDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyUpdatedSecrets.map((doc) => {
@ -445,6 +476,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
}
};
@ -466,6 +498,7 @@ export const secretReplicationServiceFactory = ({
await secretQueueService.syncSecrets({
projectId,
orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,
@ -751,6 +784,7 @@ export const secretReplicationServiceFactory = ({
await secretQueueService.syncSecrets({
projectId,
orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,

@ -180,6 +180,8 @@ export const secretRotationQueueFactory = ({
provider.template.client === TDbProviderClients.MsSqlServer
? ({
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
// when ca is provided use that
trustServerCertificate: !ca,
cryptoCredentialsDetails: ca ? { ca } : {}
} as Record<string, unknown>)
: undefined;

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
import { ProjectVersion, TableName } from "@app/db/schemas";
import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
@ -53,13 +53,14 @@ export const secretRotationServiceFactory = ({
actorAuthMethod,
projectId
}: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
return {
@ -81,13 +82,14 @@ export const secretRotationServiceFactory = ({
secretPath,
environment
}: TCreateSecretRotationDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
@ -189,13 +191,14 @@ export const secretRotationServiceFactory = ({
};
const getByProjectId = async ({ actorId, projectId, actor, actorOrgId, actorAuthMethod }: TListByProjectIdDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
@ -234,13 +237,14 @@ export const secretRotationServiceFactory = ({
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
doc.projectId,
projectId: project.id,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
@ -251,13 +255,14 @@ export const secretRotationServiceFactory = ({
const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new NotFoundError({ message: `Rotation with ID '${rotationId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
doc.projectId,
projectId: doc.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation

@ -1,9 +1,12 @@
import { Knex } from "knex";
import knex, { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretScanningGitRisksInsert } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { SecretScanningResolvedStatus, TGetOrgRisksDTO } from "./secret-scanning-types";
export type TSecretScanningDALFactory = ReturnType<typeof secretScanningDALFactory>;
@ -19,5 +22,70 @@ export const secretScanningDALFactory = (db: TDbClient) => {
}
};
return { ...gitRiskOrm, upsert };
const findByOrgId = async (orgId: string, filter: TGetOrgRisksDTO["filter"], tx?: Knex) => {
try {
// Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk)
// eslint-disable-next-line func-names
.where(`${TableName.SecretScanningGitRisk}.orgId`, orgId);
if (filter.repositoryNames) {
void sqlQuery.whereIn(`${TableName.SecretScanningGitRisk}.repositoryFullName`, filter.repositoryNames);
}
if (filter.resolvedStatus) {
if (filter.resolvedStatus !== SecretScanningResolvedStatus.All) {
const isResolved = filter.resolvedStatus === SecretScanningResolvedStatus.Resolved;
void sqlQuery.where(`${TableName.SecretScanningGitRisk}.isResolved`, isResolved);
}
}
// Select statements
void sqlQuery
.select(selectAllTableCols(TableName.SecretScanningGitRisk))
.limit(filter.limit)
.offset(filter.offset);
if (filter.orderBy) {
const orderDirection = filter.orderDirection || OrderByDirection.ASC;
void sqlQuery.orderBy(filter.orderBy, orderDirection);
}
const countQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk)
.where(`${TableName.SecretScanningGitRisk}.orgId`, orgId)
.count();
const uniqueReposQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk)
.where(`${TableName.SecretScanningGitRisk}.orgId`, orgId)
.distinct("repositoryFullName")
.select("repositoryFullName");
// we timeout long running queries to prevent DB resource issues (2 minutes)
const docs = await sqlQuery.timeout(1000 * 120);
const uniqueRepos = await uniqueReposQuery.timeout(1000 * 120);
const totalCount = await countQuery;
return {
risks: docs,
totalCount: Number(totalCount?.[0].count),
repos: uniqueRepos
.filter(Boolean)
.map((r) => r.repositoryFullName!)
.sort((a, b) => a.localeCompare(b))
};
} catch (error) {
if (error instanceof knex.KnexTimeoutError) {
throw new GatewayTimeoutError({
error,
message: "Failed to fetch secret leaks due to timeout. Add more search filters."
});
}
throw new DatabaseError({ error });
}
};
return { ...gitRiskOrm, upsert, findByOrgId };
};

@ -15,6 +15,7 @@ import { TSecretScanningDALFactory } from "./secret-scanning-dal";
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
import {
SecretScanningRiskStatus,
TGetAllOrgRisksDTO,
TGetOrgInstallStatusDTO,
TGetOrgRisksDTO,
TInstallAppSessionDTO,
@ -118,11 +119,21 @@ export const secretScanningServiceFactory = ({
return Boolean(appInstallation);
};
const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetOrgRisksDTO) => {
const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const results = await secretScanningDAL.findByOrgId(orgId, filter);
return results;
};
const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
return { risks };
return risks;
};
const updateRiskStatus = async ({
@ -189,6 +200,7 @@ export const secretScanningServiceFactory = ({
linkInstallationToOrg,
getOrgInstallationStatus,
getRisksByOrg,
getAllRisksByOrg,
updateRiskStatus,
handleRepoPushEvent,
handleRepoDeleteEvent

@ -1,4 +1,4 @@
import { TOrgPermission } from "@app/lib/types";
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
export enum SecretScanningRiskStatus {
FalsePositive = "RESOLVED_FALSE_POSITIVE",
@ -7,6 +7,12 @@ export enum SecretScanningRiskStatus {
Unresolved = "UNRESOLVED"
}
export enum SecretScanningResolvedStatus {
All = "all",
Resolved = "resolved",
Unresolved = "unresolved"
}
export type TInstallAppSessionDTO = TOrgPermission;
export type TLinkInstallSessionDTO = {
@ -16,7 +22,22 @@ export type TLinkInstallSessionDTO = {
export type TGetOrgInstallStatusDTO = TOrgPermission;
export type TGetOrgRisksDTO = TOrgPermission;
type RiskFilter = {
offset: number;
limit: number;
orderBy?: "createdAt" | "name";
orderDirection?: OrderByDirection;
repositoryNames?: string[];
resolvedStatus?: SecretScanningResolvedStatus;
};
export type TGetOrgRisksDTO = {
filter: RiskFilter;
} & TOrgPermission;
export type TGetAllOrgRisksDTO = {
filter: Omit<RiskFilter, "offset" | "limit" | "orderBy" | "orderDirection">;
} & TOrgPermission;
export type TUpdateRiskStatusDTO = {
riskId: string;

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@ -83,13 +83,14 @@ export const secretSnapshotServiceFactory = ({
actorAuthMethod,
path
}: TProjectSnapshotCountDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
@ -119,13 +120,14 @@ export const secretSnapshotServiceFactory = ({
limit = 20,
offset = 0
}: TProjectSnapshotListDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
@ -147,13 +149,14 @@ export const secretSnapshotServiceFactory = ({
const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
const snapshot = await snapshotDAL.findById(id);
if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
snapshot.projectId,
projectId: snapshot.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3;
@ -322,13 +325,14 @@ export const secretSnapshotServiceFactory = ({
if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` });
const shouldUseBridge = snapshot.projectVersion === 3;
const { permission } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
snapshot.projectId,
projectId: snapshot.projectId,
actorAuthMethod,
actorOrgId
);
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback

@ -0,0 +1,66 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSshCertificateTemplateDALFactory = ReturnType<typeof sshCertificateTemplateDALFactory>;
export const sshCertificateTemplateDALFactory = (db: TDbClient) => {
const sshCertificateTemplateOrm = ormify(db, TableName.SshCertificateTemplate);
const getById = async (id: string, tx?: Knex) => {
try {
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
.join(
TableName.SshCertificateAuthority,
`${TableName.SshCertificateAuthority}.id`,
`${TableName.SshCertificateTemplate}.sshCaId`
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`)
.where(`${TableName.SshCertificateTemplate}.id`, "=", id)
.select(selectAllTableCols(TableName.SshCertificateTemplate))
.select(
db.ref("projectId").withSchema(TableName.SshCertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
)
.first();
return certTemplate;
} catch (error) {
throw new DatabaseError({ error, name: "Get SSH certificate template by ID" });
}
};
/**
* Returns the SSH certificate template named [name] within project with id [projectId]
*/
const getByName = async (name: string, projectId: string, tx?: Knex) => {
try {
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
.join(
TableName.SshCertificateAuthority,
`${TableName.SshCertificateAuthority}.id`,
`${TableName.SshCertificateTemplate}.sshCaId`
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`)
.where(`${TableName.SshCertificateTemplate}.name`, "=", name)
.where(`${TableName.Project}.id`, "=", projectId)
.select(selectAllTableCols(TableName.SshCertificateTemplate))
.select(
db.ref("projectId").withSchema(TableName.SshCertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
)
.first();
return certTemplate;
} catch (error) {
throw new DatabaseError({ error, name: "Get SSH certificate template by name" });
}
};
return { ...sshCertificateTemplateOrm, getById, getByName };
};

@ -0,0 +1,15 @@
import { SshCertificateTemplatesSchema } from "@app/db/schemas";
export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({
id: true,
sshCaId: true,
status: true,
name: true,
ttl: true,
maxTTL: true,
allowedUsers: true,
allowedHosts: true,
allowCustomKeyIds: true,
allowUserCertificates: true,
allowHostCertificates: true
});

@ -0,0 +1,249 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
import {
SshCertTemplateStatus,
TCreateSshCertTemplateDTO,
TDeleteSshCertTemplateDTO,
TGetSshCertTemplateDTO,
TUpdateSshCertTemplateDTO
} from "./ssh-certificate-template-types";
type TSshCertificateTemplateServiceFactoryDep = {
sshCertificateTemplateDAL: Pick<
TSshCertificateTemplateDALFactory,
"transaction" | "getByName" | "create" | "updateById" | "deleteById" | "getById"
>;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TSshCertificateTemplateServiceFactory = ReturnType<typeof sshCertificateTemplateServiceFactory>;
export const sshCertificateTemplateServiceFactory = ({
sshCertificateTemplateDAL,
sshCertificateAuthorityDAL,
permissionService
}: TSshCertificateTemplateServiceFactoryDep) => {
const createSshCertTemplate = async ({
sshCaId,
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateSshCertTemplateDTO) => {
const ca = await sshCertificateAuthorityDAL.findById(sshCaId);
if (!ca) {
throw new NotFoundError({
message: `SSH CA with ID ${sshCaId} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SshCertificateTemplates
);
if (ms(ttl) > ms(maxTTL)) {
throw new BadRequestError({
message: "TTL cannot be greater than max TTL"
});
}
const newCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.projectId, tx);
if (existingTemplate) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
});
}
const certificateTemplate = await sshCertificateTemplateDAL.create(
{
sshCaId,
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds,
status: SshCertTemplateStatus.ACTIVE
},
tx
);
return certificateTemplate;
});
return { certificateTemplate: newCertificateTemplate, ca };
};
const updateSshCertTemplate = async ({
id,
status,
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateSshCertTemplateDTO) => {
const certTemplate = await sshCertificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: `SSH certificate template with ID ${id} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SshCertificateTemplates
);
const updatedCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
if (name) {
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, certTemplate.projectId, tx);
if (existingTemplate && existingTemplate.id !== id) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
});
}
}
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
throw new BadRequestError({
message: "TTL cannot be greater than max TTL"
});
}
const certificateTemplate = await sshCertificateTemplateDAL.updateById(
id,
{
status,
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds
},
tx
);
return certificateTemplate;
});
return {
certificateTemplate: updatedCertificateTemplate,
projectId: certTemplate.projectId
};
};
const deleteSshCertTemplate = async ({
id,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TDeleteSshCertTemplateDTO) => {
const certificateTemplate = await sshCertificateTemplateDAL.getById(id);
if (!certificateTemplate) {
throw new NotFoundError({
message: `SSH certificate template with ID ${id} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certificateTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SshCertificateTemplates
);
await sshCertificateTemplateDAL.deleteById(certificateTemplate.id);
return certificateTemplate;
};
const getSshCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshCertTemplateDTO) => {
const certTemplate = await sshCertificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: `SSH certificate template with ID ${id} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SshCertificateTemplates
);
return certTemplate;
};
return {
createSshCertTemplate,
updateSshCertTemplate,
deleteSshCertTemplate,
getSshCertTemplate
};
};

@ -0,0 +1,39 @@
import { TProjectPermission } from "@app/lib/types";
export enum SshCertTemplateStatus {
ACTIVE = "active",
DISABLED = "disabled"
}
export type TCreateSshCertTemplateDTO = {
sshCaId: string;
name: string;
ttl: string;
maxTTL: string;
allowUserCertificates: boolean;
allowHostCertificates: boolean;
allowedUsers: string[];
allowedHosts: string[];
allowCustomKeyIds: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateSshCertTemplateDTO = {
id: string;
status?: SshCertTemplateStatus;
name?: string;
ttl?: string;
maxTTL?: string;
allowUserCertificates?: boolean;
allowHostCertificates?: boolean;
allowedUsers?: string[];
allowedHosts?: string[];
allowCustomKeyIds?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetSshCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteSshCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;

@ -0,0 +1,14 @@
// Validates usernames or wildcard (*)
export const isValidUserPattern = (value: string): boolean => {
// Matches valid Linux usernames or a wildcard (*)
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
return userRegex.test(value);
};
// Validates hostnames, wildcard domains, or IP addresses
export const isValidHostPattern = (value: string): boolean => {
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
const hostRegex =
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
return hostRegex.test(value);
};

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

@ -0,0 +1,38 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TSshCertificateDALFactory = ReturnType<typeof sshCertificateDALFactory>;
export const sshCertificateDALFactory = (db: TDbClient) => {
const sshCertificateOrm = ormify(db, TableName.SshCertificate);
const countSshCertificatesInProject = async (projectId: string) => {
try {
interface CountResult {
count: string;
}
const query = db
.replicaNode()(TableName.SshCertificate)
.join(
TableName.SshCertificateAuthority,
`${TableName.SshCertificate}.sshCaId`,
`${TableName.SshCertificateAuthority}.id`
)
.join(TableName.Project, `${TableName.SshCertificateAuthority}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.id`, projectId);
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 SSH certificates in project" });
}
};
return {
...sshCertificateOrm,
countSshCertificatesInProject
};
};

@ -0,0 +1,14 @@
import { SshCertificatesSchema } from "@app/db/schemas";
export const sanitizedSshCertificate = SshCertificatesSchema.pick({
id: true,
sshCaId: true,
sshCertificateTemplateId: true,
serialNumber: true,
certType: true,
publicKey: true,
principals: true,
keyId: true,
notBefore: true,
notAfter: true
});

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

@ -0,0 +1,376 @@
import { execFile } from "child_process";
import crypto from "crypto";
import { promises as fs } from "fs";
import ms from "ms";
import os from "os";
import path from "path";
import { promisify } from "util";
import { TSshCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import {
isValidHostPattern,
isValidUserPattern
} from "../ssh-certificate-template/ssh-certificate-template-validators";
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
const execFileAsync = promisify(execFile);
/* eslint-disable no-bitwise */
export const createSshCertSerialNumber = () => {
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
randomBytes[0] &= 0x7f; // Ensure the most significant bit is 0 (to stay within unsigned range)
return BigInt(`0x${randomBytes.toString("hex")}`).toString(10); // Convert to decimal
};
/**
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
* We use this function because the key format generated by `ssh-keygen` is unique.
*/
export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
const privateKeyFile = path.join(tempDir, "id_key");
const publicKeyFile = `${privateKeyFile}.pub`;
let keyType: string;
let keyBits: string;
switch (keyAlgorithm) {
case CertKeyAlgorithm.RSA_2048:
keyType = "rsa";
keyBits = "2048";
break;
case CertKeyAlgorithm.RSA_4096:
keyType = "rsa";
keyBits = "4096";
break;
case CertKeyAlgorithm.ECDSA_P256:
keyType = "ecdsa";
keyBits = "256";
break;
case CertKeyAlgorithm.ECDSA_P384:
keyType = "ecdsa";
keyBits = "384";
break;
default:
throw new BadRequestError({
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
});
}
try {
// Generate the SSH key pair
// The "-N ''" sets an empty passphrase
// The keys are created in the temporary directory
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]);
// Read the generated keys
const publicKey = await fs.readFile(publicKeyFile, "utf8");
const privateKey = await fs.readFile(privateKeyFile, "utf8");
return { publicKey, privateKey };
} finally {
// Cleanup the temporary directory and all its contents
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
};
/**
* Return the SSH public key for the given SSH private key.
*/
export const getSshPublicKey = async (privateKey: string) => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
const privateKeyFile = path.join(tempDir, "id_key");
try {
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
// Run ssh-keygen to extract the public key
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
return stdout.trim();
} finally {
// Ensure that files and the temporary directory are cleaned up
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
};
/**
* Validate the requested SSH certificate type based on the SSH certificate template configuration.
*/
export const validateSshCertificateType = (template: TSshCertificateTemplates, certType: SshCertType) => {
if (!template.allowUserCertificates && certType === SshCertType.USER) {
throw new BadRequestError({ message: "Failed to validate user certificate type due to template restriction" });
}
if (!template.allowHostCertificates && certType === SshCertType.HOST) {
throw new BadRequestError({ message: "Failed to validate host certificate type due to template restriction" });
}
};
/**
* Validate the requested SSH certificate principals based on the SSH certificate template configuration.
*/
export const validateSshCertificatePrincipals = (
certType: SshCertType,
template: TSshCertificateTemplates,
principals: string[]
) => {
/**
* Validate and sanitize a principal string
*/
const validatePrincipal = (principal: string) => {
const sanitized = principal.trim();
// basic checks for empty or control characters
if (sanitized.length === 0) {
throw new BadRequestError({
message: "Principal cannot be an empty string."
});
}
if (/\r|\n|\t|\0/.test(sanitized)) {
throw new BadRequestError({
message: `Principal '${sanitized}' contains invalid whitespace or control characters.`
});
}
// disallow whitespace anywhere
if (/\s/.test(sanitized)) {
throw new BadRequestError({
message: `Principal '${sanitized}' cannot contain whitespace.`
});
}
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
if (!/^[A-Za-z0-9._-]+$/.test(sanitized)) {
throw new BadRequestError({
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
});
}
// disallow leading hyphen to avoid potential argument-like inputs
if (sanitized.startsWith("-")) {
throw new BadRequestError({
message: `Principal '${sanitized}' cannot start with a hyphen.`
});
}
// length restriction (adjust as needed)
if (sanitized.length > 64) {
throw new BadRequestError({
message: `Principal '${sanitized}' is too long.`
});
}
return sanitized;
};
// Sanitize and validate all principals using the helper
const sanitizedPrincipals = principals.map(validatePrincipal);
switch (certType) {
case SshCertType.USER: {
if (template.allowedUsers.length === 0) {
throw new BadRequestError({
message: "No allowed users are configured in the SSH certificate template."
});
}
const allowsAllUsers = template.allowedUsers.includes("*") ?? false;
sanitizedPrincipals.forEach((principal) => {
if (principal === "*") {
throw new BadRequestError({
message: `Principal '*' is not allowed for user certificates.`
});
}
if (allowsAllUsers && !isValidUserPattern(principal)) {
throw new BadRequestError({
message: `Principal '${principal}' does not match a valid user pattern.`
});
}
if (!allowsAllUsers && !template.allowedUsers.includes(principal)) {
throw new BadRequestError({
message: `Principal '${principal}' is not in the list of allowed users.`
});
}
});
break;
}
case SshCertType.HOST: {
if (template.allowedHosts.length === 0) {
throw new BadRequestError({
message: "No allowed hosts are configured in the SSH certificate template."
});
}
const allowsAllHosts = template.allowedHosts.includes("*") ?? false;
sanitizedPrincipals.forEach((principal) => {
if (principal.includes("*")) {
throw new BadRequestError({
message: `Principal '${principal}' with wildcards is not allowed for host certificates.`
});
}
if (allowsAllHosts && !isValidHostPattern(principal)) {
throw new BadRequestError({
message: `Principal '${principal}' does not match a valid host pattern.`
});
}
if (
!allowsAllHosts &&
!template.allowedHosts.some((allowedHost) => {
if (allowedHost.startsWith("*.")) {
const baseDomain = allowedHost.slice(2); // Remove the leading "*."
return principal.endsWith(`.${baseDomain}`);
}
return principal === allowedHost;
})
) {
throw new BadRequestError({
message: `Principal '${principal}' is not in the list of allowed hosts or domains.`
});
}
});
break;
}
default:
throw new BadRequestError({
message: "Failed to validate SSH certificate principals due to unrecognized requested certificate type"
});
}
};
/**
* Validate the requested SSH certificate TTL based on the SSH certificate template configuration.
*/
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl?: string) => {
if (!ttl) {
// use default template ttl
return Math.ceil(ms(template.ttl) / 1000);
}
if (ms(ttl) > ms(template.maxTTL)) {
throw new BadRequestError({
message: "Failed TTL validation due to TTL being greater than configured max TTL on template"
});
}
return Math.ceil(ms(ttl) / 1000);
};
/**
* Validate the requested SSH certificate key ID to ensure
* that it only contains alphanumeric characters with no spaces.
*/
export const validateSshCertificateKeyId = (keyId: string) => {
const regex = /^[A-Za-z0-9-]+$/;
if (!regex.test(keyId)) {
throw new BadRequestError({
message:
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
});
}
if (keyId.length > 50) {
throw new BadRequestError({
message: "keyId can only be up to 50 characters long."
});
}
};
/**
* Validate the format of the SSH public key
*/
const validateSshPublicKey = async (publicKey: string) => {
const validPrefixes = ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"];
const startsWithValidPrefix = validPrefixes.some((prefix) => publicKey.startsWith(`${prefix} `));
if (!startsWithValidPrefix) {
throw new BadRequestError({ message: "Failed to validate SSH public key format: unsupported key type." });
}
// write the key to a temp file and run `ssh-keygen -l -f`
// check to see if OpenSSH can read/interpret the public key
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-pubkey-"));
const pubKeyFile = path.join(tempDir, "key.pub");
try {
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile]);
} catch (error) {
throw new BadRequestError({
message: "Failed to validate SSH public key format: could not be parsed."
});
} finally {
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
};
/**
* Create an SSH certificate for a user or host.
*/
export const createSshCert = async ({
template,
caPrivateKey,
clientPublicKey,
keyId,
principals,
requestedTtl,
certType
}: TCreateSshCertDTO) => {
// validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(template, certType);
// validate if the requested [principals] are valid for the given [certType] under the template configuration
validateSshCertificatePrincipals(certType, template, principals);
// validate if the requested TTL is valid under the template configuration
const ttl = validateSshCertificateTtl(template, requestedTtl);
validateSshCertificateKeyId(keyId);
await validateSshPublicKey(clientPublicKey);
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-cert-"));
const publicKeyFile = path.join(tempDir, "user_key.pub");
const privateKeyFile = path.join(tempDir, "ca_key");
const signedPublicKeyFile = path.join(tempDir, "user_key-cert.pub");
const serialNumber = createSshCertSerialNumber();
// Build `ssh-keygen` arguments for signing
// Using an array avoids shell injection issues
const sshKeygenArgs = [
certType === "host" ? "-h" : null, // host certificate if needed
"-s",
privateKeyFile, // path to SSH CA private key
"-I",
keyId, // identity (key ID)
"-n",
principals.join(","), // principals
"-V",
`+${ttl}s`, // validity (TTL in seconds)
"-z",
serialNumber, // serial number
publicKeyFile // public key file to sign
].filter(Boolean) as string[];
try {
// Write public and private keys to the temp directory
await fs.writeFile(publicKeyFile, clientPublicKey, { mode: 0o600 });
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
// Execute the signing process
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
// Read the signed public key from the generated cert file
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
return { serialNumber, signedPublicKey, ttl };
} finally {
// Cleanup the temporary directory and all its contents
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
};

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