Compare commits

..

377 Commits

Author SHA1 Message Date
Daniel Hougaard
a24ef46d7d requested changes 2025-01-28 20:44:45 +01:00
Daniel Hougaard
939ee892e0 chore: cleanup 2025-01-28 01:02:18 +01:00
Daniel Hougaard
27efc908e2 feat(audit-logs): query by secret path 2025-01-27 15:53:07 +01:00
Daniel Hougaard
a93bfa69c9 Merge pull request #3042 from Infisical/daniel/fix-approvals-for-personal-secrets
fix: approvals triggering for personal secrets
2025-01-25 04:50:19 +01:00
Scott Wilson
08a0550cd7 fix: correct dependency arra 2025-01-24 19:21:33 -08:00
Daniel Hougaard
d7503573b1 Merge pull request #3041 from Infisical/daniel/remove-caching-from-docs
docs: update node guid eand remove cache references
2025-01-25 04:15:53 +01:00
Daniel Hougaard
b5a89edeed Update node.mdx 2025-01-25 03:59:06 +01:00
Daniel Hougaard
860eaae4c8 fix: approvals triggering for personal secrets 2025-01-25 03:44:43 +01:00
Daniel Hougaard
c7a4b6c4e9 docs: update node guid eand remove cache references 2025-01-25 03:12:36 +01:00
Daniel Hougaard
c12c6dcc6e Merge pull request #2987 from Infisical/daniel/k8s-multi-managed-secrets
feat(k8-operator/infisicalsecret-crd): multiple secret references
2025-01-25 02:59:07 +01:00
Daniel Hougaard
8741414cfa Update routeTree.gen.ts 2025-01-24 18:28:48 +01:00
Daniel Hougaard
b8d29793ec fix: rename managedSecretReferneces to managedKubeSecretReferences 2025-01-24 18:26:56 +01:00
Daniel Hougaard
92013dbfbc fix: routes 2025-01-24 18:26:34 +01:00
Daniel Hougaard
c5319588fe chore: fix routes geneartion 2025-01-24 18:26:23 +01:00
Daniel Hougaard
9efb8eaf78 Update infisical-secret-crd.mdx 2025-01-24 18:24:26 +01:00
Daniel Hougaard
dfc973c7f7 chore(k8-operator): update helm 2025-01-24 18:24:26 +01:00
Daniel Hougaard
3013d1977c docs(k8-operator): updated infisicalsecret crd docs 2025-01-24 18:24:26 +01:00
Daniel Hougaard
f358e8942d feat(k8-operator): multiple managed secrets 2025-01-24 18:24:26 +01:00
Maidul Islam
c3970d1ea2 Merge pull request #3038 from isaiahmartin847/typo-fix/Role-based-Access-Controls
Fixed the typo in the Role-based Access Controls docs.
2025-01-24 01:30:34 -05:00
isaiahmartin847
2dc00a638a fixed the typo in the /access-controls/role-based-access-controls page in the docs. 2025-01-23 23:15:40 -07:00
Daniel Hougaard
bab9c1f454 Merge pull request #3024 from Infisical/team-city-integration-fix
Fix: UI Fix for Team City Integrations Create Page
2025-01-23 18:14:32 +01:00
Akhil Mohan
2bd4770fb4 Merge pull request #3035 from akhilmhdh/fix/env-ui
feat: updated ui validation for env to 64 like api
2025-01-23 16:32:04 +05:30
=
31905fab6e feat: updated ui validation for env to 64 like api 2025-01-23 16:26:13 +05:30
Maidul Islam
784acf16d0 Merge pull request #3032 from Infisical/correct-app-connections-docs
Improvements: Minor Secret Sync improvements and Correct App Connections Env Vars and Move Sync/Connections to Groups in Docs
2025-01-23 03:29:33 -05:00
Maidul Islam
114b89c952 Merge pull request #3033 from Infisical/daniel/update-python-docs
docs(guides): updated python guide
2025-01-23 03:28:11 -05:00
Scott Wilson
81420198cb fix: display aws connection credentials error and sync status on details page 2025-01-22 21:00:01 -08:00
Daniel Hougaard
b949708f45 docs(sso): fixed azure attributes typo 2025-01-23 05:20:44 +01:00
Daniel Hougaard
2a6b6b03b9 docs(guides): updated python guide 2025-01-23 05:20:26 +01:00
Scott Wilson
0ff18e277f docs: redact info in image 2025-01-22 20:02:03 -08:00
Scott Wilson
e093f70301 docs: add new aws connection images 2025-01-22 19:58:24 -08:00
Scott Wilson
8e2ff18f35 docs: improve aws connection docs 2025-01-22 19:58:06 -08:00
Scott Wilson
3fbfecf7a9 docs: correct aws env vars in aws connection self-hosted docs 2025-01-22 18:46:36 -08:00
Scott Wilson
9087def21c docs: correct github connection env vars and move connections and syncs to group 2025-01-22 18:40:24 -08:00
Scott Wilson
89c6ab591a Merge pull request #3031 from Infisical/project-list-pagination-fix
Fix: Project List/Grid Pagination Behavior
2025-01-22 16:27:59 -08:00
Maidul Islam
235a33a01c Update deployment-pipeline.yml 2025-01-22 18:42:05 -05:00
Scott Wilson
dd6c217dc8 fix: include page/page size in dependencies array on filtered projects 2025-01-22 14:18:38 -08:00
Scott Wilson
78b1b5583a Merge pull request #2998 from Infisical/secret-syncs-feature
Feature: Secret Syncs
2025-01-22 12:00:57 -08:00
Scott Wilson
8f2a504fd0 improvements: address feedback 2025-01-22 10:50:24 -08:00
Maidul Islam
1d5b629d8f Merge pull request #3006 from akhilmhdh/feat/region-flag
Added region flag for eu in cli
2025-01-22 13:21:14 -05:00
Maidul Islam
14f895cae2 Merge pull request #3026 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
Maidul Islam
58a97852f6 Merge pull request #3027 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
Tuan Dang
52d801bce5 Add Infisical SSH to README 2025-01-21 22:36:55 -08:00
Scott Wilson
c92c160709 chore: move migration to latest 2025-01-21 21:51:35 -08:00
Scott Wilson
71ca7a82db Merge pull request #3022 from Infisical/vercel-project-help-text
Improvement: Vercel Integration Project Permission Helper Text
2025-01-21 21:43:53 -08:00
Scott Wilson
6f799b478d chore: remove unused component 2025-01-21 21:34:41 -08:00
Scott Wilson
a89e6b6e58 chore: resolve merge conflicts 2025-01-21 21:30:18 -08:00
Scott Wilson
99ca9e04f8 improvements: final adjustments/improvements 2025-01-21 20:21:29 -08:00
Scott Wilson
586dbd79b0 fix: fix team city integrations create page 2025-01-21 18:37:01 -08:00
Daniel Hougaard
6cdc71b9b1 Merge pull request #3023 from Infisical/fix-org-sidebar-check
Fix: Correct Display Org Sidebar Check
2025-01-22 03:02:43 +01:00
Scott Wilson
f88d6a183f fix: correct display org sidebar check 2025-01-21 17:46:14 -08:00
Scott Wilson
fa82d4953e improvement: adjust casing 2025-01-21 16:05:11 -08:00
Scott Wilson
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
Maidul Islam
86acf88a13 Merge pull request #3021 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
Maidul Islam
151edc7efa Merge pull request #3020 from Infisical/remove-enterprise-identity-limit
Remove Enterprise Member and Identity Limits for SAML/LDAP Login
2025-01-21 13:17:16 -05:00
Tuan Dang
5fa7f56285 Remove member and identity limits for enterprise saml and ldap login case 2025-01-21 10:12:23 -08:00
Scott Wilson
810b27d121 Merge pull request #3019 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
Scott Wilson
51fe7450ae improvement: propogate upstream error on audit log stream setup 2025-01-21 09:54:33 -08:00
Maidul Islam
938c06a2ed Merge pull request #3018 from Infisical/misc/added-more-scoping-for-namespace
misc: added more scoping logic for namespace installation
2025-01-21 11:09:45 -05:00
Sheen Capadngan
38d431ec77 misc: added more scoping logic for namespace installation 2025-01-21 23:57:11 +08:00
Maidul Islam
d202fdf5c8 Merge pull request #3017 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
Sheen
5c9b46dfba Merge pull request #3015 from Infisical/misc/address-scim-email-verification-patch-issue
misc: address scim email verification patch issue
2025-01-21 20:23:08 +08:00
Sheen Capadngan
a516e50984 misc: address scim email verification patch issue 2025-01-21 17:01:34 +08:00
Scott Wilson
3c1fc024c2 improvements: address feedback 2025-01-20 22:17:20 -08:00
Sheen
569439f208 Merge pull request #2999 from Infisical/misc/added-handling-for-case-enforcement
misc: added handling of secret case enforcement
2025-01-21 13:58:48 +08:00
Maidul Islam
9afc282679 Update deployment-pipeline.yml 2025-01-21 00:49:31 -05:00
Maidul Islam
8db85cfb84 Add slack alert 2025-01-21 00:04:14 -05:00
Maidul Islam
664b2f0089 add concurrency to git workflow 2025-01-20 23:52:47 -05:00
Maidul Islam
5e9bd3a7c6 Merge pull request #3014 from Infisical/expanded-secret-overview-adjustment
Fix: Adjust Secret Overview Expanded Secret View
2025-01-20 21:39:39 -05:00
Maidul Islam
2c13af6db3 correct helm verion 2025-01-20 21:37:13 -05:00
Scott Wilson
ec9171d0bc fix: adjust secret overview expanded secret view to accomodate nav restructure 2025-01-20 18:30:54 -08:00
Vlad Matsiiako
81362bec8f Merge pull request #3013 from Infisical/daniel/cli-fix-2
fix: saml redirect failing due to blocked pop-ups
2025-01-20 13:21:32 -08:00
Daniel Hougaard
5a4d7541a2 Update SelectOrgPage.tsx 2025-01-20 22:13:52 +01:00
Maidul Islam
3c97c45455 others => other 2025-01-20 15:26:15 -05:00
Maidul Islam
4f015d77fb Merge pull request #3003 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
Maidul Islam
4a6f759900 Merge pull request #3007 from akhilmhdh/feat/base64-decode-in-operator
Base64 decode in operator
2025-01-19 13:50:56 -05:00
Maidul Islam
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
Maidul Islam
8de9ddfb8b update k8s operator doc 2025-01-19 11:29:55 -05:00
Maidul Islam
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
Scott Wilson
3ba396f7fa Merge pull request #3005 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
Scott Wilson
9c561266ed improvement: add secret key to aws parameter store integreation error message 2025-01-17 08:51:41 -08:00
Sheen
36fef11d91 Merge pull request #3004 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
Akhil Mohan
742932c4a0 Merge pull request #2993 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
Sheen Capadngan
57a77ae5f1 misc: incremented chart and operator versions 2025-01-17 19:16:48 +08:00
Sheen Capadngan
7c9564c7dc misc: added missing install CRD flag for multi installation 2025-01-17 19:09:12 +08:00
Sheen
736aecebf8 Merge pull request #3001 from Infisical/fix/address-org-invite-resend
fix: address org invite resend issues
2025-01-17 18:53:32 +08:00
Sheen Capadngan
16748357d7 misc: addressed comments 2025-01-17 03:50:01 +08:00
Daniel Hougaard
12863b389b Merge pull request #2997 from Infisical/daniel/unique-group-names
feat(groups): unique names
2025-01-16 20:41:07 +01:00
Scott Wilson
6341b7e989 improvement: update intial sync logic to account for replica reads 2025-01-16 09:48:21 -08:00
Sheen Capadngan
c592ff00a6 fix: address org invite resend 2025-01-17 01:42:35 +08:00
Daniel Hougaard
ef87086272 fix(groups): unique names, requested changes 2025-01-16 17:38:54 +01:00
Sheen Capadngan
bd459d994c misc: finalized error message 2025-01-16 19:55:21 +08:00
Sheen Capadngan
440f93f392 misc: added handling for case enforcement 2025-01-16 19:10:00 +08:00
Scott Wilson
bc32d6cbbf chore: revert mint openapi url 2025-01-15 22:42:09 -08:00
Scott Wilson
0cf3115830 chore: remove needless comment 2025-01-15 22:39:54 -08:00
Scott Wilson
65f2e626ae chore: optimize import path, add comment context and removed commented out code 2025-01-15 22:37:20 -08:00
Scott Wilson
8b3e3152a4 chore: remove outdated comment 2025-01-15 22:30:27 -08:00
Scott Wilson
661b31f762 chore: remove concurrency from testing 2025-01-15 22:24:47 -08:00
Scott Wilson
e78ad1147b fix: various ui and github sync fixes 2025-01-15 22:20:33 -08:00
Scott Wilson
473efa91f0 feature: secret sync base + aws parameter store & github 2025-01-15 21:52:50 -08:00
Daniel Hougaard
b440e918ac Merge pull request #2988 from Infisical/daniel/audit-logs-searchability
feat(radar): pagination and filtering
2025-01-16 01:24:24 +01:00
Daniel Hougaard
439f253350 Merge pull request #2981 from Infisical/daniel/secret-scans-export
feat(radar): export radar data
2025-01-16 01:22:05 +01:00
Daniel Hougaard
4e68304262 fix: type check 2025-01-16 01:08:43 +01:00
Daniel Hougaard
c4d0896609 feat(groups): unique names 2025-01-16 01:01:01 +01:00
Daniel Hougaard
b8115d481c Merge pull request #2989 from Infisical/daniel/audit-log-docs
docs(audit-logs): audit log streams structure
2025-01-15 18:07:24 +01:00
Daniel Hougaard
5fdec97319 requested changes 2025-01-15 17:50:30 +01:00
Daniel Hougaard
755bb1679a requested changes 2025-01-15 17:32:53 +01:00
Daniel Hougaard
7142e7a6c6 Update secret-scanning-dal.ts 2025-01-15 16:39:08 +01:00
Daniel Hougaard
11ade92b5b Update secret-scanning-service.ts 2025-01-15 16:39:08 +01:00
Daniel Hougaard
4147725260 feat(radar): pagination & filtering 2025-01-15 16:39:08 +01:00
Daniel Hougaard
c359cf162f requested changes 2025-01-15 16:37:42 +01:00
Sheen Capadngan
9d04b648fa misc: add org id display to org settings 2025-01-15 20:21:10 +08:00
Daniel Hougaard
9edfdb7234 docs(audit-logs): audit log streams structure 2025-01-15 02:33:21 +01:00
Daniel Hougaard
ff74e020fc requested changes 2025-01-15 01:55:10 +01:00
Akhil Mohan
6ee446e574 Merge pull request #2979 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
Maidul Islam
c806059b11 Merge pull request #2985 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
Sheen Capadngan
5ad419c079 misc: updated comment 2025-01-15 00:49:53 +08:00
Sheen Capadngan
80f72e8040 misc: added context 2025-01-15 00:48:19 +08:00
Maidul Islam
0cef728617 Merge pull request #2986 from Infisical/misc/addressed-cf-pages-error-304
fix: addressed cloudflare pages error 304
2025-01-14 10:42:42 -05:00
Sheen Capadngan
0a735422ed misc: standardized log 2025-01-14 23:39:47 +08:00
Sheen Capadngan
8370a0d9c0 misc: added log 2025-01-14 23:29:04 +08:00
Sheen Capadngan
71af662998 fix: addressed cloudflare pages error 304 2025-01-14 23:18:38 +08:00
Sheen Capadngan
27e391f7e0 misc: add oidc saml handling for login check 2025-01-14 21:02:41 +08:00
Maidul Islam
2187c7588c update cache control to be no store 2025-01-13 23:58:36 -05:00
Maidul Islam
8ab7e8360d add cache busting param 2025-01-13 22:53:04 -05:00
Maidul Islam
957cab3117 Merge pull request #2982 from Infisical/fix-copy-user-id
Fix: Copy Username on User Details Page
2025-01-13 20:06:11 -05:00
Scott Wilson
4bf16f68fc fix: correctly copy user email to clipboard 2025-01-13 17:03:08 -08:00
Daniel Hougaard
ab3ee775bb feat(radar): export radar data 2025-01-14 00:53:40 +01:00
Maidul Islam
2a86e6f4d1 add cache contro to cloudflare pages integration 2025-01-13 17:17:39 -05:00
Maidul Islam
194fbb79f2 Merge pull request #2975 from Infisical/daniel/custom-cors
feat(api): custom cors settings
2025-01-13 14:36:19 -05:00
Maidul Islam
faaba8deb7 add metabase link to on call guide 2025-01-13 11:16:49 -05:00
Sheen
ae21b157a9 Merge pull request #2980 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
Maidul Islam
6167c70a74 Merge pull request #2973 from Infisical/daniel/push-secret-docs
docs: small k8s docs improvements
2025-01-13 10:39:45 -05:00
Sheen Capadngan
0ecf75cbdb misc: scoped down to AWS-related errors 2025-01-13 23:18:24 +08:00
Sheen Capadngan
3f8aa0fa4b misc: added error propagation for aws kms 2025-01-13 23:00:40 +08:00
Daniel Hougaard
6487c83bda docs: requested changes 2025-01-13 14:35:36 +01:00
Daniel Hougaard
c08fbbdab2 Update app.ts 2025-01-13 13:59:25 +01:00
Maidul Islam
a4aa65bb81 add on call to hand book 2025-01-13 01:27:50 -05:00
Daniel Hougaard
258d19cbe4 Merge pull request #2974 from Infisical/daniel/shared-secret-audit-logs
feat(audit-logs): shared secrets audit logs
2025-01-10 22:29:05 +01:00
Daniel Hougaard
de91356127 Update envars.mdx 2025-01-10 22:24:57 +01:00
Daniel Hougaard
ccb07942de feat(api): custom cors settings 2025-01-10 22:23:19 +01:00
Daniel Hougaard
3d278b0925 feat(audit-logs): shared secrets audit logs 2025-01-10 21:37:10 +01:00
Daniel Hougaard
956fb2efb4 docs: better k8s pre-req 2025-01-10 20:16:30 +01:00
Daniel Hougaard
13894261ce docs: small k8s docs improvements 2025-01-10 20:10:00 +01:00
Maidul Islam
d7ffa70906 Merge pull request #2972 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
Sheen
b8fa7c5bb6 Merge pull request #2969 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
Sheen Capadngan
2baacfcd8f misc: add support for array values 2025-01-11 02:07:04 +08:00
Maidul Islam
31c11f7d2a Merge pull request #2964 from Infisical/vmatsiiako-typo-patch-1
Update infisical-dynamic-secret-crd.mdx
2025-01-10 12:43:40 -05:00
Sheen
c5f06dece4 Merge pull request #2957 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
Daniel Hougaard
662e79ac98 Merge pull request #2971 from Infisical/daniel/audit-log-timestamp-bug
fix(audit-logs): time conversion bug
2025-01-10 17:23:58 +01:00
Daniel Hougaard
17249d603b fix: add delete secrets event type to frontend 2025-01-10 17:15:26 +01:00
Daniel Hougaard
9bdff9c504 fix: explicit postgres time conversion 2025-01-10 17:15:14 +01:00
Maidul Islam
4552ce6ca4 Merge pull request #2970 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
Maidul Islam
ba4b8801eb Merge pull request #2965 from akhilmhdh/fix/broken-secret-creation
Resolve self signed error for mssql
2025-01-10 11:01:54 -05:00
Sheen Capadngan
36a5f728a1 misc: add secret key indicator for failed AWS integration syncs 2025-01-11 00:00:17 +08:00
Sheen Capadngan
502429d914 misc: resolve shared secret within org and added login redirect 2025-01-10 23:55:21 +08:00
Sheen
27abfa4fff Merge pull request #2966 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
Sheen Capadngan
4bc9bca287 removed undefined type 2025-01-10 19:08:49 +08:00
Sheen Capadngan
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
Vlad Matsiiako
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
Sheen
9cde1995c7 Merge pull request #2962 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
Maidul Islam
3ed3856c85 Merge pull request #2963 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
Sheen Capadngan
02dc23425c fix: address infinite hang when infisical run watch fails 2025-01-10 02:29:39 +08:00
Maidul Islam
01534c3525 Merge pull request #2847 from Infisical/daniel/k8s-dynamic-secrets
feat(k8-operator): dynamic secrets
2025-01-09 12:45:30 -05:00
Maidul Islam
325ce73b9f fix typo 2025-01-09 12:36:52 -05:00
Maidul Islam
1639bda3f6 remove copy-paste and make redeploy docs specific 2025-01-09 12:23:46 -05:00
Maidul Islam
b5b91c929f fix kubernetes docs 2025-01-09 12:11:48 -05:00
Sheen
9bc549ca8c Merge pull request #2960 from Infisical/feat/add-initial-sync-behavior-for-gitlab
feat: add initial sync behavior for gitlab
2025-01-10 01:01:17 +08:00
Sheen Capadngan
cedc88d83a misc: removed comment 2025-01-10 00:42:16 +08:00
Sheen
f866a810c1 Merge pull request #2950 from Infisical/feat/secret-access-list
feat: secret access list
2025-01-10 00:40:56 +08:00
Sheen Capadngan
0c034d69ac misc: removed raw from api 2025-01-10 00:32:31 +08:00
Daniel Hougaard
e29f7f656c docs(k8s): better documentation layout 2025-01-09 16:07:07 +01:00
Sheen Capadngan
858e569d4d feat: add initial sync behavior for gitlab 2025-01-09 20:43:14 +08:00
Vlad Matsiiako
5d8f32b774 Merge pull request #2955 from thomas-infisical/patch-1
Doc: Update Nov & Dec Changelogs
2025-01-08 21:42:49 -08:00
Maidul Islam
bb71f5eb7e Merge pull request #2956 from Infisical/daniel/update-k8s-manifest
fix: k8's manifest out of sync
2025-01-08 17:25:21 -05:00
Maidul Islam
30b431a255 Merge pull request #2958 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
max-infisical
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
Maidul Islam
8528f3fd2a Merge pull request #2897 from Infisical/feat/resource-metadata
feat: resource metadata
2025-01-08 14:47:01 -05:00
Sheen Capadngan
60749cfc43 misc: made is active optional for integration patch 2025-01-09 02:19:24 +08:00
Daniel Hougaard
eb358bcafd Update install-secrets-operator.yaml 2025-01-08 16:26:50 +01:00
Thomas
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
Sheen
a74f0170da Merge pull request #2954 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
Sheen Capadngan
f0dc5ec876 misc: add secret access insights to license fns 2025-01-08 20:56:33 +08:00
Sheen Capadngan
c2453f0c84 misc: finalized ui 2025-01-08 20:50:20 +08:00
Sheen Capadngan
2819c8519e misc: added license checks 2025-01-08 20:31:04 +08:00
Sheen Capadngan
616b013b12 Merge remote-tracking branch 'origin/main' into feat/secret-access-list 2025-01-08 20:08:16 +08:00
Sheen
0b9d890a51 Merge pull request #2953 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
Sheen Capadngan
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
Sheen Capadngan
f5adc4d9f3 misc: removed unnecessary type assertion 2025-01-08 18:44:58 +08:00
Sheen Capadngan
34354994d8 fix: address sso redirect and project role creation 2025-01-08 18:29:25 +08:00
Sheen Capadngan
d7c3192099 feat: frontend integration 2025-01-08 18:24:40 +08:00
Maidul Islam
1576358805 Merge pull request #2947 from Infisical/auth0-saml
Add Support for Auth0 SAML
2025-01-07 15:04:44 -05:00
Scott Wilson
e6103d2d3f docs: update initial saml setup steps 2025-01-07 11:45:07 -08:00
Maidul Islam
8bf8bc77c9 Merge pull request #2951 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
Sheen Capadngan
74b95d92ab feat: backend setup 2025-01-08 02:48:47 +08:00
Akhil Mohan
6d3793beff Merge pull request #2949 from akhilmhdh/fix/broken-image
Fixed broke image
2025-01-08 00:18:38 +05:30
Scott Wilson
0df41f3391 Merge pull request #2948 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
Tuan Dang
0cefd6f837 Run linter 2025-01-08 01:41:22 +07:00
Scott Wilson
5e9dc0b98d clarify enterprise plan for user group feature upgrade modal 2025-01-07 10:38:09 -08:00
Tuan Dang
f632847dc6 Merge branch 'main', remote-tracking branch 'origin' into auth0-saml 2025-01-08 01:35:11 +07:00
Tuan Dang
faa6d1cf40 Add support for Auth0 SAML 2025-01-08 01:34:03 +07:00
Maidul Islam
7fb18870e3 Merge pull request #2946 from akhilmhdh/feat/updated-saml-error-message
Updated saml error message
2025-01-07 10:57:03 -05:00
Maidul Islam
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
Sheen Capadngan
291d29ec41 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2025-01-07 19:15:33 +08:00
Akhil Mohan
b726187ba3 Merge pull request #2917 from mr-ssd/patch-1
docs: add note for dynamic secrets as paid feature
2025-01-07 14:12:50 +05:30
Akhil Mohan
d98ff32b07 Merge pull request #2919 from mr-ssd/patch-2
docs: add note Approval Workflows as paid feature
2025-01-07 14:12:12 +05:30
Sheen
1fa510b32f Merge pull request #2944 from Infisical/feat/target-specific-azure-key-vault-tenant
feat: target specific azure key vault tenant
2025-01-07 14:05:14 +08:00
Maidul Islam
c57f0d8120 Merge pull request #2945 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
Sheen Capadngan
00490f2cff misc: made secret path input show correct folders based on env in integrations 2025-01-07 12:08:09 +08:00
Sheen Capadngan
ee58f538c0 feat: target specific azure key vault tenant 2025-01-07 11:53:17 +08:00
Maidul Islam
0fa20f7839 Merge pull request #2943 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
Scott Wilson
40ef75d3bd fix: use force flag when deleting secrets from aws secret manager integration 2025-01-06 16:11:32 -08:00
Maidul Islam
26af13453c Merge pull request #2942 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
Daniel Hougaard
aa39451bc2 fix: generated files 2025-01-06 22:03:56 +01:00
Daniel Hougaard
f5548b3e8c Merge branch 'heads/main' into daniel/k8s-dynamic-secrets 2025-01-06 22:00:21 +01:00
Maidul Islam
2659ea7170 Merge pull request #2940 from Infisical/misc/updated-openssh-installation-for-fips
misc: updated openssh installation for fips
2025-01-06 14:08:07 -05:00
Sheen Capadngan
d2e3f504fd misc: updated openssh installation for fips 2025-01-07 03:04:57 +08:00
Maidul Islam
ca4151a34d Merge pull request #2935 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
Maidul Islam
96b8e7fda8 Merge pull request #2930 from Infisical/vmatsiiako-wiki-patch-1 2025-01-01 18:12:48 -05:00
Vlad Matsiiako
93b9108aa3 Update onboarding.mdx 2025-01-01 15:04:50 -08:00
Sheen
99017ea1ae Merge pull request #2923 from Infisical/akhilmhdh-patch-azure
fix: resolved azure app config api not pulling all keys
2024-12-30 23:19:54 +08:00
Akhil Mohan
f32588112e fix: resolved azure app config api not pulling all keys 2024-12-29 19:51:59 +05:30
Maidul Islam
f9b0b6700a Merge pull request #2922 from Infisical/feat/authenticate-key-vault-against-specific-tenant
feat: auth key vault against specific tenant
2024-12-29 08:18:52 -05:00
Maidul Islam
b45d9398f0 Merge pull request #2920 from Infisical/vmatsiiako-intercom-patch-1 2024-12-27 21:47:36 -05:00
Maidul Islam
1d1140237f Update azure-app-configuration.mdx 2024-12-27 00:59:49 -05:00
Maidul Islam
937560fd8d Update azure-app-configuration.mdx 2024-12-27 00:58:48 -05:00
Maidul Islam
5f4b7b9ea7 Merge pull request #2921 from Infisical/patch-azure-label
Azure App Config Label patch
2024-12-27 00:40:42 -05:00
Maidul Islam
05139820a5 add docs for label and refereces 2024-12-26 16:36:27 -05:00
Sheen Capadngan
7f6bc3ecfe misc: added additional note for azure app config 2024-12-26 19:07:45 +08:00
Sheen Capadngan
d8cc000ad1 feat: authenticate key vault against specific tenant 2024-12-26 19:07:16 +08:00
Maidul Islam
8fc03c06d9 handle empty label 2024-12-26 01:17:47 -05:00
Vlad Matsiiako
50ceedf39f Remove intercom from docs 2024-12-25 16:40:45 -08:00
BlackMagiq
550096e72b Merge pull request #2787 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
Sida Say
1190ca2d77 docs: add note Approval Workflows as paid feature 2024-12-25 15:27:38 +07:00
Sida Say
2fb60201bc add not for dynamic secrets as paid feature 2024-12-25 14:17:27 +07:00
Sheen Capadngan
e763a6f683 misc: added default for metadataSyncMode 2024-12-23 21:29:51 +08:00
Sheen Capadngan
cb1b006118 misc: add secret metadata to batch update 2024-12-23 14:46:23 +08:00
Sheen Capadngan
356e7c5958 feat: added approval handling for metadata in replicate 2024-12-23 14:36:41 +08:00
Maidul Islam
634b500244 Merge pull request #2907 from Infisical/temp-hide-app-connection-docs 2024-12-20 18:53:20 -05:00
Scott Wilson
54b4d4ae55 docs: temp hide app connections 2024-12-20 15:07:23 -08:00
Sheen Capadngan
1a68765f15 feat: secret approval and replication 2024-12-21 02:51:47 +08:00
BlackMagiq
2f6dab3f63 Merge pull request #2901 from Infisical/ssh-cli-docs
Documentation for SSH Command in CLI
2024-12-20 08:49:38 -08:00
Sheen Capadngan
ae07d38c19 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2024-12-20 14:23:14 +08:00
Tuan Dang
e9564f5231 Fix ssh cli docs based on review 2024-12-19 22:12:32 -08:00
Tuan Dang
05cdca9202 Add docs for SSH CLI 2024-12-19 19:47:24 -08:00
BlackMagiq
5ab0c66dee Merge pull request #2898 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
Tuan Dang
f5a0641671 Add requirement for ssh issue credentials command to include either outFilePath or addToAgent flag 2024-12-19 11:55:44 -08:00
Maidul Islam
2843818395 Merge pull request #2899 from Infisical/misc/add-custom-metrics-for-errors
misc: added metric for api errors
2024-12-19 14:15:13 -05:00
Sheen Capadngan
025b4b8761 feat: aws secret manager metadata sync 2024-12-19 23:54:47 +08:00
Sheen Capadngan
ef688efc8d feat: integrated secret metadata to ui 2024-12-19 22:02:20 +08:00
Sheen Capadngan
8c98565715 feat: completed basic CRUD 2024-12-19 17:47:39 +08:00
Tuan Dang
f97f98b2c3 Add ability to load retrieved ssh credentials into ssh agent with new addToAgent flag 2024-12-18 23:27:11 -08:00
Sheen Capadngan
e9358cd1d8 misc: backend setup + create secret metadata 2024-12-19 15:05:16 +08:00
Daniel Hougaard
4daaf80caa fix: better naming 2024-12-18 02:56:13 +01:00
Daniel Hougaard
cf7768d8e5 Merge branch 'daniel/k8-push-secret' into daniel/k8s-dynamic-secrets 2024-12-18 01:41:17 +01:00
Daniel Hougaard
e76d2f58ea fix: move fixes from different branch 2024-12-18 01:39:32 +01:00
Daniel Hougaard
36a13d182f requested changes 2024-12-12 06:13:55 +04:00
Daniel Hougaard
8b26670d73 fix(k8s): dynamic secret structual change 2024-12-11 22:25:02 +04:00
Daniel Hougaard
35d3581e23 fix(k8s): fixed dynamic secret bugs 2024-12-11 22:22:52 +04:00
Daniel Hougaard
0edf0dac98 fix(k8-operator): PushSecret CRd causing endless snapshot updates 2024-12-10 04:31:55 +04:00
Daniel Hougaard
a757ea22a1 fix(k8-operator): improvements for dynamic secrets 2024-12-09 00:22:22 +04:00
Daniel Hougaard
74df374998 Update infisicaldynamicsecret-crd.yaml 2024-12-08 23:24:38 +04:00
Daniel Hougaard
925a594a1b feat(k8-operator): dynamic secrets status conditions logging 2024-12-08 23:24:30 +04:00
Daniel Hougaard
36af975594 docs(k8-operator): k8's dynamic secret docs 2024-12-08 22:42:29 +04:00
Daniel Hougaard
ee54d460a0 fix(k8-operator): update charts 2024-12-08 21:30:28 +04:00
Daniel Hougaard
3c32d8dd90 fix(k8-operator): helm 2024-12-08 21:30:28 +04:00
Daniel Hougaard
9b50d451ec fix(k8-operator): common types support 2024-12-08 21:30:28 +04:00
Daniel Hougaard
7ede4e2cf5 fix(k8-operator): moved template 2024-12-08 21:30:28 +04:00
Daniel Hougaard
4552f0efa4 feat(k8-operator): dynamic secrets 2024-12-08 21:30:28 +04:00
Daniel Hougaard
0d35273857 feat(k8-operator): push secrets 2024-12-08 21:30:09 +04:00
Daniel Hougaard
5ad8dab250 fix(k8-operator): resource-based finalizer names 2024-12-08 21:29:45 +04:00
Daniel Hougaard
92a80b3314 fix(k8-operator): helm and cleanup 2024-12-08 21:21:14 +04:00
Daniel Hougaard
01dcbb0122 updated env slugs 2024-12-07 05:22:21 +04:00
Daniel Hougaard
adb0819102 Added RBAC 2024-12-07 05:22:21 +04:00
Daniel Hougaard
41ba111a69 Update PROJECT 2024-12-07 05:22:21 +04:00
Daniel Hougaard
1b48ce21be Update conditions.go 2024-12-07 05:22:21 +04:00
Daniel Hougaard
2f922d6343 fix: requested changes 2024-12-07 05:22:21 +04:00
Daniel Hougaard
e67b0540dd cleanup and resource seperation 2024-12-07 05:22:21 +04:00
Daniel Hougaard
a78455fde6 remove print 2024-12-07 05:22:21 +04:00
Daniel Hougaard
967dac9be6 docs(k8-operator): push secrets 2024-12-07 05:22:21 +04:00
Daniel Hougaard
922b245780 feat(k8-operator): push secrets 2024-12-07 05:22:21 +04:00
mohammad riyaz
242595fceb changed getCaCerts key from ca-cert -> ca-certs 2024-11-24 21:05:39 +05:30
1838 changed files with 57644 additions and 46406 deletions

View File

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

View File

@@ -5,6 +5,10 @@ permissions:
id-token: write id-token: write
contents: read contents: read
concurrency:
group: "infisical-core-deployment"
cancel-in-progress: true
jobs: jobs:
infisical-tests: infisical-tests:
name: Integration tests name: Integration tests
@@ -97,7 +101,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }} image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info" environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service - 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: with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }} task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-gamma-stage service: infisical-core-gamma-stage
@@ -113,10 +117,6 @@ jobs:
steps: steps:
- uses: twingate/github-action@v1 - uses: twingate/github-action@v1
with: 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 }} service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -153,12 +153,37 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }} image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info" environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service - 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: with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }} task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform service: infisical-core-platform
cluster: infisical-core-platform cluster: infisical-core-platform
wait-for-service-stability: true 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: production-eu:
name: EU production deploy name: EU production deploy
@@ -204,9 +229,34 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }} image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info" environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service - 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: with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }} task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform service: infisical-core-platform
cluster: infisical-core-platform cluster: infisical-core-platform
wait-for-service-stability: true 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 }}>"

View File

@@ -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 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
docs/mint.json:generic-api-key:651 docs/mint.json:generic-api-key:651
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134 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

View File

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

View File

@@ -12,7 +12,7 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./ COPY frontend/package.json frontend/package-lock.json ./
# Install dependencies # Install dependencies
RUN npm ci --only-production --ignore-scripts RUN npm ci --only-production --ignore-scripts
@@ -27,17 +27,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
COPY /frontend . COPY /frontend .
ENV NODE_ENV production ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST ENV VITE_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID ENV VITE_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION 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 ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build # Build
RUN npm run build RUN npm run build
@@ -49,20 +48,10 @@ WORKDIR /app
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 non-root-user RUN adduser --system --uid 1001 non-root-user
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
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
USER non-root-user USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
## ##
## BACKEND ## BACKEND
## ##
@@ -159,14 +148,11 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates
## set pre baked keys ## set pre baked keys
ARG POSTHOG_API_KEY ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ ENV POSTHOG_API_KEY=$POSTHOG_API_KEY
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ ENV INTERCOM_ID=$INTERCOM_ID
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \ ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --from=backend-runner /app /backend COPY --from=backend-runner /app /backend
@@ -189,4 +175,4 @@ EXPOSE 443
USER non-root-user USER non-root-user
CMD ["./standalone-entrypoint.sh"] CMD ["./standalone-entrypoint.sh"]

View File

@@ -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 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. - **[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. - **[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. - **[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. - **[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. - **[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):
- **[Cryptographic 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. - **[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: ### 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)). - **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. - **[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. - **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)**: Track every action taken on the platform.

View File

@@ -26,6 +26,7 @@
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0", "@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0", "@fastify/session": "^10.7.0",
"@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0", "@google-cloud/kms": "^4.5.0",
@@ -5406,6 +5407,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
} }
@@ -5545,6 +5547,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lukeed/ms": "^2.0.1", "@lukeed/ms": "^2.0.1",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
@@ -5563,16 +5566,85 @@
} }
}, },
"node_modules/@fastify/static": { "node_modules/@fastify/static": {
"version": "6.12.0", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz",
"integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==", "integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@fastify/accept-negotiator": "^1.0.0", "@fastify/accept-negotiator": "^1.0.0",
"@fastify/send": "^2.0.0", "@fastify/send": "^2.0.0",
"content-disposition": "^0.5.3", "content-disposition": "^0.5.3",
"fastify-plugin": "^4.0.0", "fastify-plugin": "^4.0.0",
"glob": "^8.0.1", "fastq": "^1.17.0",
"p-limit": "^3.1.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": { "node_modules/@fastify/swagger": {
@@ -5599,6 +5671,20 @@
"yaml": "^2.2.2" "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": { "node_modules/@google-cloud/kms": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
@@ -6062,9 +6148,10 @@
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
}, },
"node_modules/@lukeed/ms": { "node_modules/@lukeed/ms": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
"integrity": "sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==", "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -13879,9 +13966,9 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.1", "version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
@@ -13903,7 +13990,7 @@
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.10", "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
@@ -13918,6 +14005,10 @@
}, },
"engines": { "engines": {
"node": ">= 0.10.0" "node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-session": { "node_modules/express-session": {
@@ -17388,15 +17479,16 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@@ -18383,9 +18475,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.10", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {

View File

@@ -134,6 +134,7 @@
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0", "@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0", "@fastify/session": "^10.7.0",
"@fastify/static": "^7.0.4",
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0", "@google-cloud/kms": "^4.5.0",

View File

@@ -80,6 +80,7 @@ import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service"; import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { TSecretReplicationServiceFactory } from "@app/services/secret-replication/secret-replication-service"; import { TSecretReplicationServiceFactory } from "@app/services/secret-replication/secret-replication-service";
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service"; import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { TSecretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service"; import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service"; import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSlackServiceFactory } from "@app/services/slack/slack-service"; import { TSlackServiceFactory } from "@app/services/slack/slack-service";
@@ -210,6 +211,7 @@ declare module "fastify" {
projectTemplate: TProjectTemplateServiceFactory; projectTemplate: TProjectTemplateServiceFactory;
totp: TTotpServiceFactory; totp: TTotpServiceFactory;
appConnection: TAppConnectionServiceFactory; appConnection: TAppConnectionServiceFactory;
secretSync: TSecretSyncServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer

View File

@@ -218,6 +218,9 @@ import {
TRateLimit, TRateLimit,
TRateLimitInsert, TRateLimitInsert,
TRateLimitUpdate, TRateLimitUpdate,
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate,
TSamlConfigs, TSamlConfigs,
TSamlConfigsInsert, TSamlConfigsInsert,
TSamlConfigsUpdate, TSamlConfigsUpdate,
@@ -369,6 +372,7 @@ import {
TExternalGroupOrgRoleMappingsInsert, TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate TExternalGroupOrgRoleMappingsUpdate
} from "@app/db/schemas/external-group-org-role-mappings"; } from "@app/db/schemas/external-group-org-role-mappings";
import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs";
import { import {
TSecretV2TagJunction, TSecretV2TagJunction,
TSecretV2TagJunctionInsert, TSecretV2TagJunctionInsert,
@@ -887,10 +891,16 @@ declare module "knex/types/tables" {
TProjectSplitBackfillIdsInsert, TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate TProjectSplitBackfillIdsUpdate
>; >;
[TableName.ResourceMetadata]: KnexOriginal.CompositeTableType<
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate
>;
[TableName.AppConnection]: KnexOriginal.CompositeTableType< [TableName.AppConnection]: KnexOriginal.CompositeTableType<
TAppConnections, TAppConnections,
TAppConnectionsInsert, TAppConnectionsInsert,
TAppConnectionsUpdate TAppConnectionsUpdate
>; >;
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
} }
} }

View File

@@ -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");
});
}
}

View File

@@ -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"]);
});
}

View File

@@ -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();
}
});
}

View File

@@ -0,0 +1,50 @@
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.SecretSync))) {
await knex.schema.createTable(TableName.SecretSync, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description");
t.string("destination").notNullable();
t.boolean("isAutoSyncEnabled").notNullable().defaultTo(true);
t.integer("version").defaultTo(1).notNullable();
t.jsonb("destinationConfig").notNullable();
t.jsonb("syncOptions").notNullable();
// we're including projectId in addition to folder ID because we allow folderId to be null (if the folder
// is deleted), to preserve sync configuration
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("folderId");
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("SET NULL");
t.uuid("connectionId").notNullable();
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.timestamps(true, true, true);
// sync secrets to destination
t.string("syncStatus");
t.string("lastSyncJobId");
t.string("lastSyncMessage");
t.datetime("lastSyncedAt");
// import secrets from destination
t.string("importStatus");
t.string("lastImportJobId");
t.string("lastImportMessage");
t.datetime("lastImportedAt");
// remove secrets from destination
t.string("removeStatus");
t.string("lastRemoveJobId");
t.string("lastRemoveMessage");
t.datetime("lastRemovedAt");
});
await createOnUpdateTrigger(knex, TableName.SecretSync);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SecretSync);
await dropOnUpdateTrigger(knex, TableName.SecretSync);
}

View File

@@ -71,6 +71,7 @@ export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles"; export * from "./project-user-membership-roles";
export * from "./projects"; export * from "./projects";
export * from "./rate-limit"; export * from "./rate-limit";
export * from "./resource-metadata";
export * from "./saml-configs"; export * from "./saml-configs";
export * from "./scim-tokens"; export * from "./scim-tokens";
export * from "./secret-approval-policies"; export * from "./secret-approval-policies";

View File

@@ -80,6 +80,7 @@ export enum TableName {
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
// used by both identity and users // used by both identity and users
IdentityMetadata = "identity_metadata", IdentityMetadata = "identity_metadata",
ResourceMetadata = "resource_metadata",
ScimToken = "scim_tokens", ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicy = "access_approval_policies",
AccessApprovalPolicyApprover = "access_approval_policies_approvers", AccessApprovalPolicyApprover = "access_approval_policies_approvers",
@@ -130,7 +131,8 @@ export enum TableName {
WorkflowIntegrations = "workflow_integrations", WorkflowIntegrations = "workflow_integrations",
SlackIntegrations = "slack_integrations", SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs", ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections" AppConnection = "app_connections",
SecretSync = "secret_syncs"
} }
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
@@ -214,3 +216,12 @@ export enum ProjectType {
KMS = "kms", KMS = "kms",
SSH = "ssh" 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"
}

View File

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

View File

@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { 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>>;

View File

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

View File

@@ -0,0 +1,40 @@
// 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 SecretSyncsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
destination: z.string(),
isAutoSyncEnabled: z.boolean().default(true),
version: z.number().default(1),
destinationConfig: z.unknown(),
syncOptions: z.unknown(),
projectId: z.string(),
folderId: z.string().uuid().nullable().optional(),
connectionId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
syncStatus: z.string().nullable().optional(),
lastSyncJobId: z.string().nullable().optional(),
lastSyncMessage: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional(),
importStatus: z.string().nullable().optional(),
lastImportJobId: z.string().nullable().optional(),
lastImportMessage: z.string().nullable().optional(),
lastImportedAt: z.date().nullable().optional(),
removeStatus: z.string().nullable().optional(),
lastRemoveJobId: z.string().nullable().optional(),
lastRemoveMessage: z.string().nullable().optional(),
lastRemovedAt: z.date().nullable().optional()
});
export type TSecretSyncs = z.infer<typeof SecretSyncsSchema>;
export type TSecretSyncsInsert = Omit<z.input<typeof SecretSyncsSchema>, TImmutableDBKeys>;
export type TSecretSyncsUpdate = Partial<Omit<z.input<typeof SecretSyncsSchema>, TImmutableDBKeys>>;

View File

@@ -22,6 +22,7 @@ import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-rou
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router"; import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
import { registerSecretRotationRouter } from "./secret-rotation-router"; import { registerSecretRotationRouter } from "./secret-rotation-router";
import { registerSecretRouter } from "./secret-router";
import { registerSecretScanningRouter } from "./secret-scanning-router"; import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSecretVersionRouter } from "./secret-version-router"; import { registerSecretVersionRouter } from "./secret-version-router";
import { registerSnapshotRouter } from "./snapshot-router"; import { registerSnapshotRouter } from "./snapshot-router";
@@ -92,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerLdapRouter, { prefix: "/ldap" }); await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretRouter, { prefix: "/secrets" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(registerGroupRouter, { prefix: "/groups" }); await server.register(registerGroupRouter, { prefix: "/groups" });
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" }); await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });

View File

@@ -24,6 +24,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
), ),
name: z.string().trim(), name: z.string().trim(),
description: z.string().trim().nullish(), description: z.string().trim().nullish(),
// TODO(scott): once UI refactored permissions: OrgPermissionSchema.array()
permissions: z.any().array() permissions: z.any().array()
}), }),
response: { response: {
@@ -96,6 +97,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
name: z.string().trim().optional(), name: z.string().trim().optional(),
description: z.string().trim().nullish(), description: z.string().trim().nullish(),
// TODO(scott): once UI refactored permissions: OrgPermissionSchema.array().optional()
permissions: z.any().array().optional() permissions: z.any().array().optional()
}), }),
response: { response: {

View File

@@ -84,7 +84,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
samlConfig.audience = `spn:${ssoConfig.issuer}`; 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; samlConfig.wantAssertionsSigned = false;
} }
@@ -123,7 +126,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
`email: ${email} firstName: ${profile.firstName as string}` `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 || {}) const userMetadata = Object.keys(profile.attributes || {})

View File

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

View File

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

View File

@@ -1,9 +1,13 @@
import { z } from "zod"; import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas"; 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 { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -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({ server.route({
url: "/organization/:organizationId/risks", url: "/organization/:organizationId/risks",
method: "GET", method: "GET",
@@ -105,20 +148,46 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
}, },
schema: { schema: {
params: z.object({ organizationId: z.string().trim() }), 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: { 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]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const { risks } = await server.services.secretScanning.getRisksByOrg({ const { risks, totalCount, repos } = await server.services.secretScanning.getRisksByOrg({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, 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 };
} }
}); });

View File

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

View File

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

View File

@@ -93,7 +93,7 @@ export const auditLogStreamServiceFactory = ({
} }
) )
.catch((err) => { .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 encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
const logStream = await auditLogStreamDAL.create({ const logStream = await auditLogStreamDAL.create({

View File

@@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => {
offset = 0, offset = 0,
actorId, actorId,
actorType, actorType,
secretPath,
eventType, eventType,
eventMetadata eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & { }: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string; actorId?: string;
actorType?: ActorType; actorType?: ActorType;
secretPath?: string;
eventType?: EventType[]; eventType?: EventType[];
eventMetadata?: Record<string, string>; eventMetadata?: Record<string, string>;
}, },
@@ -88,6 +90,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
}); });
} }
if (projectId && secretPath) {
void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]);
}
// Filter by actor type // Filter by actor type
if (actorType) { if (actorType) {
void sqlQuery.where("actor", actorType); void sqlQuery.where("actor", actorType);
@@ -100,10 +106,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
// Filter by date range // Filter by date range
if (startDate) { if (startDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate); void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
} }
if (endDate) { 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) // we timeout long running queries to prevent DB resource issues (2 minutes)

View File

@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -26,13 +27,14 @@ export const auditLogServiceFactory = ({
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => { const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
// Filter logs for specific project // Filter logs for specific project
if (filter.projectId) { if (filter.projectId) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
filter.projectId, projectId: filter.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
} else { } else {
// Organization-wide logs // Organization-wide logs
@@ -44,10 +46,6 @@ export const auditLogServiceFactory = ({
actorOrgId actorOrgId
); );
/**
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
* to the organization level ✅
*/
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
} }
@@ -62,6 +60,7 @@ export const auditLogServiceFactory = ({
actorId: filter.auditLogActorId, actorId: filter.auditLogActorId,
actorType: filter.actorType, actorType: filter.actorType,
eventMetadata: filter.eventMetadata, eventMetadata: filter.eventMetadata,
secretPath: filter.secretPath,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
}); });
@@ -79,7 +78,8 @@ export const auditLogServiceFactory = ({
} }
// add all cases in which project id or org id cannot be added // add all cases in which project id or org id cannot be added
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" }); if (!data.projectId && !data.orgId)
throw new BadRequestError({ message: "Must specify either project id or org id" });
} }
return auditLogQueue.pushToLog(data); return auditLogQueue.pushToLog(data);

View File

@@ -13,6 +13,13 @@ import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums";
import {
TCreateSecretSyncDTO,
TDeleteSecretSyncDTO,
TSecretSyncRaw,
TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types";
export type TListProjectAuditLogDTO = { export type TListProjectAuditLogDTO = {
filter: { filter: {
@@ -25,13 +32,14 @@ export type TListProjectAuditLogDTO = {
projectId?: string; projectId?: string;
auditLogActorId?: string; auditLogActorId?: string;
actorType?: ActorType; actorType?: ActorType;
secretPath?: string;
eventMetadata?: Record<string, string>; eventMetadata?: Record<string, string>;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateAuditLogDTO = { export type TCreateAuditLogDTO = {
event: Event; event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor; actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
orgId?: string; orgId?: string;
projectId?: string; projectId?: string;
} & BaseAuthData; } & BaseAuthData;
@@ -226,10 +234,22 @@ export enum EventType {
DELETE_PROJECT_TEMPLATE = "delete-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_CONNECTIONS = "get-app-connections",
GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details",
GET_APP_CONNECTION = "get-app-connection", GET_APP_CONNECTION = "get-app-connection",
CREATE_APP_CONNECTION = "create-app-connection", CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection", UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-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",
GET_SECRET_SYNCS = "get-secret-syncs",
GET_SECRET_SYNC = "get-secret-sync",
CREATE_SECRET_SYNC = "create-secret-sync",
UPDATE_SECRET_SYNC = "update-secret-sync",
DELETE_SECRET_SYNC = "delete-secret-sync",
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -252,6 +272,8 @@ interface ScimClientActorMetadata {}
interface PlatformActorMetadata {} interface PlatformActorMetadata {}
interface UnknownUserActorMetadata {}
export interface UserActor { export interface UserActor {
type: ActorType.USER; type: ActorType.USER;
metadata: UserActorMetadata; metadata: UserActorMetadata;
@@ -267,6 +289,11 @@ export interface PlatformActor {
metadata: PlatformActorMetadata; metadata: PlatformActorMetadata;
} }
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
metadata: UnknownUserActorMetadata;
}
export interface IdentityActor { export interface IdentityActor {
type: ActorType.IDENTITY; type: ActorType.IDENTITY;
metadata: IdentityActorMetadata; metadata: IdentityActorMetadata;
@@ -1883,6 +1910,15 @@ interface GetAppConnectionsEvent {
}; };
} }
interface GetAvailableAppConnectionsDetailsEvent {
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS;
metadata: {
app?: AppConnection;
count: number;
connectionIds: string[];
};
}
interface GetAppConnectionEvent { interface GetAppConnectionEvent {
type: EventType.GET_APP_CONNECTION; type: EventType.GET_APP_CONNECTION;
metadata: { metadata: {
@@ -1907,6 +1943,107 @@ interface DeleteAppConnectionEvent {
}; };
} }
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;
};
}
interface GetSecretSyncsEvent {
type: EventType.GET_SECRET_SYNCS;
metadata: {
destination?: SecretSync;
count: number;
syncIds: string[];
};
}
interface GetSecretSyncEvent {
type: EventType.GET_SECRET_SYNC;
metadata: {
destination: SecretSync;
syncId: string;
};
}
interface CreateSecretSyncEvent {
type: EventType.CREATE_SECRET_SYNC;
metadata: Omit<TCreateSecretSyncDTO, "projectId"> & { syncId: string };
}
interface UpdateSecretSyncEvent {
type: EventType.UPDATE_SECRET_SYNC;
metadata: TUpdateSecretSyncDTO;
}
interface DeleteSecretSyncEvent {
type: EventType.DELETE_SECRET_SYNC;
metadata: TDeleteSecretSyncDTO;
}
interface SecretSyncSyncSecretsEvent {
type: EventType.SECRET_SYNC_SYNC_SECRETS;
metadata: Pick<
TSecretSyncRaw,
"syncOptions" | "destinationConfig" | "destination" | "syncStatus" | "connectionId" | "folderId"
> & {
syncId: string;
syncMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
interface SecretSyncImportSecretsEvent {
type: EventType.SECRET_SYNC_IMPORT_SECRETS;
metadata: Pick<
TSecretSyncRaw,
"syncOptions" | "destinationConfig" | "destination" | "importStatus" | "connectionId" | "folderId"
> & {
syncId: string;
importMessage: string | null;
jobId: string;
jobRanAt: Date;
importBehavior: SecretSyncImportBehavior;
};
}
interface SecretSyncRemoveSecretsEvent {
type: EventType.SECRET_SYNC_REMOVE_SECRETS;
metadata: Pick<
TSecretSyncRaw,
"syncOptions" | "destinationConfig" | "destination" | "removeStatus" | "connectionId" | "folderId"
> & {
syncId: string;
removeMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -2080,7 +2217,19 @@ export type Event =
| DeleteProjectTemplateEvent | DeleteProjectTemplateEvent
| ApplyProjectTemplateEvent | ApplyProjectTemplateEvent
| GetAppConnectionsEvent | GetAppConnectionsEvent
| GetAvailableAppConnectionsDetailsEvent
| GetAppConnectionEvent | GetAppConnectionEvent
| CreateAppConnectionEvent | CreateAppConnectionEvent
| UpdateAppConnectionEvent | UpdateAppConnectionEvent
| DeleteAppConnectionEvent; | DeleteAppConnectionEvent
| CreateSharedSecretEvent
| DeleteSharedSecretEvent
| ReadSharedSecretEvent
| GetSecretSyncsEvent
| GetSecretSyncEvent
| CreateSecretSyncEvent
| UpdateSecretSyncEvent
| DeleteSecretSyncEvent
| SecretSyncSyncSecretsEvent
| SecretSyncImportSecretsEvent
| SecretSyncRemoveSecretsEvent;

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => { const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
const db = knex({ const db = knex({
client: providerInputs.client, client: providerInputs.client,
connection: { connection: {
@@ -43,7 +45,16 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
user: providerInputs.username, user: providerInputs.username,
password: providerInputs.password, password: providerInputs.password,
ssl, 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 acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
}); });

View File

@@ -1,7 +1,9 @@
import { KMSServiceException } from "@aws-sdk/client-kms";
import { STSServiceException } from "@aws-sdk/client-sts";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; 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 { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -71,7 +73,16 @@ export const externalKmsServiceFactory = ({
switch (provider.type) { switch (provider.type) {
case KmsProviders.Aws: 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 // if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey(); const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput); sanitizedProviderInput = JSON.stringify(newProviderInput);

View File

@@ -32,7 +32,7 @@ type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
@@ -88,12 +88,26 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges) if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
const group = await groupDAL.create({ const group = await groupDAL.transaction(async (tx) => {
name, const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), if (existingGroup) {
orgId: actorOrgId, throw new BadRequestError({
role: isCustomRole ? OrgMembershipRole.Custom : role, message: `Failed to create group with name '${name}'. Group with the same name already exists`
roleId: customRole?.id });
}
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; return group;
@@ -145,21 +159,36 @@ export const groupServiceFactory = ({
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
} }
const [updatedGroup] = await groupDAL.update( const updatedGroup = await groupDAL.transaction(async (tx) => {
{ if (name) {
id: group.id const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
},
{ if (existingGroup && existingGroup.id !== id) {
name, throw new BadRequestError({
slug: slug ? slugify(slug) : undefined, message: `Failed to update group with name '${name}'. Group with the same name already exists`
...(role });
? { }
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
} }
);
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; return updatedGroup;
}; };

View File

@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
@@ -55,24 +55,26 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
ActorType.IDENTITY, actor: ActorType.IDENTITY,
identityId, actorId: identityId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @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 // @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}` message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
}); });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
); );
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
ActorType.IDENTITY, actor: ActorType.IDENTITY,
identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @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 // @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}` message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
}); });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
); );
const { permission: identityRolePermission } = await permissionService.getProjectPermission( const { permission: identityRolePermission } = await permissionService.getProjectPermission({
ActorType.IDENTITY, actor: ActorType.IDENTITY,
identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges) if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); 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}` message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
}); });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
@@ -294,13 +301,14 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
@@ -329,13 +337,14 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })

View File

@@ -2,6 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
@@ -62,25 +63,27 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
ActorType.IDENTITY, actor: ActorType.IDENTITY,
identityId, actorId: identityId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @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 // @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) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
ActorType.IDENTITY, actor: ActorType.IDENTITY,
identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @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 // @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) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { permission: identityRolePermission } = await permissionService.getProjectPermission( const { permission: identityRolePermission } = await permissionService.getProjectPermission({
ActorType.IDENTITY, actor: ActorType.IDENTITY,
identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges) if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
@@ -299,13 +306,14 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
@@ -341,13 +349,14 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,

View File

@@ -476,14 +476,14 @@ export const ldapConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); 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 // limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." 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 // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,12 @@
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import {
CASL_ACTION_SCHEMA_ENUM,
CASL_ACTION_SCHEMA_NATIVE_ENUM
} from "@app/ee/services/permission/permission-schemas";
import { PermissionConditionSchema } from "@app/ee/services/permission/permission-types";
import { PermissionConditionOperators } from "@app/lib/casl";
export enum OrgPermissionActions { export enum OrgPermissionActions {
Read = "read", Read = "read",
@@ -7,6 +15,14 @@ export enum OrgPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum OrgPermissionAppConnectionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
Connect = "connect"
}
export enum OrgPermissionAdminConsoleAction { export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects" AccessAllProjects = "access-all-projects"
} }
@@ -31,6 +47,10 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections" AppConnections = "app-connections"
} }
export type AppConnectionSubjectFields = {
connectionId: string;
};
export type OrgPermissionSet = export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role] | [OrgPermissionActions, OrgPermissionSubjects.Role]
@@ -47,9 +67,109 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections] | [
OrgPermissionAppConnectionActions,
(
| OrgPermissionSubjects.AppConnections
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const AppConnectionConditionSchema = z
.object({
connectionId: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
export const OrgPermissionSchema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(OrgPermissionSubjects.Workspace).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Settings).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.IncidentAccount).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Sso).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Groups).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.SecretScanning).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Billing).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Identity).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Kms).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.AuditLogs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.ProjectTemplates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({
subject: z.literal(OrgPermissionSubjects.AppConnections).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAppConnectionActions).describe(
"Describe what action an entity can take."
),
conditions: AppConnectionConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(OrgPermissionSubjects.AdminConsole).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
"Describe what action an entity can take."
)
})
]);
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions // ws permissions
@@ -125,10 +245,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates); can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Read, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Create, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Edit, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -160,7 +281,7 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
return rules; return rules;
}; };

View File

@@ -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) => { const getProjectPermission = async (userId: string, projectId: string) => {
try { try {
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
@@ -414,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) => { const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
try { try {
const docs = await db const docs = await db
@@ -568,6 +1123,9 @@ export const permissionDALFactory = (db: TDbClient) => {
getOrgPermission, getOrgPermission,
getOrgIdentityPermission, getOrgIdentityPermission,
getProjectPermission, getProjectPermission,
getProjectIdentityPermission getProjectIdentityPermission,
getProjectUserPermissions,
getProjectIdentityPermissions,
getProjectGroupPermissions
}; };
}; };

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
z
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
.transform((el) => (typeof el === "string" ? [el] : el));
export const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));

View File

@@ -1,3 +1,6 @@
import { ActionProjectType } from "@app/db/schemas";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export type TBuildProjectPermissionDTO = { export type TBuildProjectPermissionDTO = {
permissions?: unknown; permissions?: unknown;
role: string; role: string;
@@ -7,3 +10,34 @@ export type TBuildOrgPermissionDTO = {
permissions?: unknown; permissions?: unknown;
role: string; 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;
};

View File

@@ -4,9 +4,9 @@ import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars"; import handlebars from "handlebars";
import { import {
ActionProjectType,
OrgMembershipRole, OrgMembershipRole,
ProjectMembershipRole, ProjectMembershipRole,
ProjectType,
ServiceTokenScopes, ServiceTokenScopes,
TIdentityProjectMemberships, TIdentityProjectMemberships,
TProjectMemberships TProjectMemberships
@@ -23,7 +23,14 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal"; import { TPermissionDALFactory } from "./permission-dal";
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns"; 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 { import {
buildServiceTokenProjectPermission, buildServiceTokenProjectPermission,
projectAdminPermissions, projectAdminPermissions,
@@ -193,12 +200,13 @@ export const permissionServiceFactory = ({
}; };
// user permission for a project in an organization // user permission for a project in an organization
const getUserProjectPermission = async ( const getUserProjectPermission = async ({
userId: string, userId,
projectId: string, projectId,
authMethod: ActorAuthMethod, authMethod,
userOrgId?: string userOrgId,
): Promise<TProjectPermissionRT<ActorType.USER>> => { actionProjectType
}: TGetUserProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.USER>> => {
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" }); if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" });
@@ -219,6 +227,12 @@ export const permissionServiceFactory = ({
validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); 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 // join two permissions and pass to build the final permission set
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges = const additionalPrivileges =
@@ -256,13 +270,6 @@ export const permissionServiceFactory = ({
return { return {
permission, permission,
membership: userProjectPermission, membership: userProjectPermission,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== userProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
});
}
},
hasRole: (role: string) => hasRole: (role: string) =>
userProjectPermission.roles.findIndex( userProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@@ -270,11 +277,12 @@ export const permissionServiceFactory = ({
}; };
}; };
const getIdentityProjectPermission = async ( const getIdentityProjectPermission = async ({
identityId: string, identityId,
projectId: string, projectId,
identityOrgId: string | undefined identityOrgId,
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => { actionProjectType
}: TGetIdentityProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!identityProjectPermission) if (!identityProjectPermission)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -293,6 +301,12 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" }); throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" });
} }
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.`
});
}
const rolePermissions = const rolePermissions =
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges = const additionalPrivileges =
@@ -331,13 +345,6 @@ export const permissionServiceFactory = ({
return { return {
permission, permission,
membership: identityProjectPermission, membership: identityProjectPermission,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== identityProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
});
}
},
hasRole: (role: string) => hasRole: (role: string) =>
identityProjectPermission.roles.findIndex( identityProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@@ -345,11 +352,12 @@ export const permissionServiceFactory = ({
}; };
}; };
const getServiceTokenProjectPermission = async ( const getServiceTokenProjectPermission = async ({
serviceTokenId: string, serviceTokenId,
projectId: string, projectId,
actorOrgId: string | undefined actorOrgId,
) => { actionProjectType
}: TGetServiceTokenProjectPermissionArg) => {
const serviceToken = await serviceTokenDAL.findById(serviceTokenId); const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${serviceTokenId}' not found` }); if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${serviceTokenId}' not found` });
@@ -373,17 +381,16 @@ export const permissionServiceFactory = ({
}); });
} }
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== serviceTokenProject.type) {
throw new BadRequestError({
message: `The project is of type ${serviceTokenProject.type}. Operations of type ${actionProjectType} are not allowed.`
});
}
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return { return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
membership: undefined, membership: undefined
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== serviceTokenProject.type) {
throw new BadRequestError({
message: `The project is of type ${serviceTokenProject.type}. Operations of type ${productType} are not allowed.`
});
}
}
}; };
}; };
@@ -392,7 +399,6 @@ export const permissionServiceFactory = ({
permission: MongoAbility<ProjectPermissionSet, MongoQuery>; permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: undefined; membership: undefined;
hasRole: (arg: string) => boolean; hasRole: (arg: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
} // service token doesn't have both membership and roles } // service token doesn't have both membership and roles
: { : {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>; permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
@@ -402,23 +408,156 @@ export const permissionServiceFactory = ({
roles: Array<{ role: string }>; roles: Array<{ role: string }>;
}; };
hasRole: (role: string) => boolean; hasRole: (role: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
}; };
const getProjectPermission = async <T extends ActorType>( const getProjectPermissions = async (projectId: string) => {
type: T, // fetch user permissions
id: string, const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId);
projectId: string, const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => {
actorAuthMethod: ActorAuthMethod, const rolePermissions =
actorOrgId: string | undefined userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
): Promise<TProjectPermissionRT<T>> => { const additionalPrivileges =
switch (type) { 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: 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: 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: 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: default:
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid actor provided", message: "Invalid actor provided",
@@ -455,6 +594,7 @@ export const permissionServiceFactory = ({
getOrgPermission, getOrgPermission,
getUserProjectPermission, getUserProjectPermission,
getProjectPermission, getProjectPermission,
getProjectPermissions,
getOrgPermissionByRole, getOrgPermissionByRole,
getProjectPermissionByRole, getProjectPermissionByRole,
buildOrgPermission, buildOrgPermission,

View File

@@ -1,6 +1,10 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import {
CASL_ACTION_SCHEMA_ENUM,
CASL_ACTION_SCHEMA_NATIVE_ENUM
} from "@app/ee/services/permission/permission-schemas";
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl"; import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
@@ -30,6 +34,16 @@ export enum ProjectPermissionDynamicSecretActions {
Lease = "lease" Lease = "lease"
} }
export enum ProjectPermissionSecretSyncActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
SyncSecrets = "sync-secrets",
ImportSecrets = "import-secrets",
RemoveSecrets = "remove-secrets"
}
export enum ProjectPermissionSub { export enum ProjectPermissionSub {
Role = "role", Role = "role",
Member = "member", Member = "member",
@@ -60,7 +74,8 @@ export enum ProjectPermissionSub {
PkiAlerts = "pki-alerts", PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections", PkiCollections = "pki-collections",
Kms = "kms", Kms = "kms",
Cmek = "cmek" Cmek = "cmek",
SecretSyncs = "secret-syncs"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -140,6 +155,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
@@ -147,14 +163,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
z
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
.transform((el) => (typeof el === "string" ? [el] : el));
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
// akhilmhdh: don't modify this for v2 // akhilmhdh: don't modify this for v2
// if you want to update create a new schema // if you want to update create a new schema
const SecretConditionV1Schema = z const SecretConditionV1Schema = z
@@ -392,10 +400,15 @@ const GeneralPermissionSchema = [
}), }),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
)
}) })
]; ];
@@ -549,6 +562,18 @@ const buildAdminPermissionRules = () => {
], ],
ProjectPermissionSub.Cmek ProjectPermissionSub.Cmek
); );
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
return rules; return rules;
}; };
@@ -713,6 +738,19 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.Cmek ProjectPermissionSub.Cmek
); );
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
return rules; return rules;
}; };
@@ -746,6 +784,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
return rules; return rules;
}; };

View File

@@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
@@ -55,21 +55,23 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
if (!projectMembership) if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` }); throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission( const { permission: targetUserPermission } = await permissionService.getProjectPermission({
ActorType.USER, actor: ActorType.USER,
projectMembership.userId, actorId: projectMembership.userId,
projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @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 // @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}'` 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, actor,
actorId, actorId,
projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission( const { permission: targetUserPermission } = await permissionService.getProjectPermission({
ActorType.USER, actor: ActorType.USER,
projectMembership.userId, actorId: projectMembership.userId,
projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @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 // @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}'` 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, actor,
actorId, actorId,
projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); 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}'` 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, actor,
actorId, actorId,
projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return { return {
@@ -286,13 +292,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
if (!projectMembership) if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` }); throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectMembership.projectId, projectId: projectMembership.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find( const userPrivileges = await projectUserAdditionalPrivilegeDAL.find(

View File

@@ -421,14 +421,14 @@ export const samlConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); 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 // limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." 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 // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."

View File

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

View File

@@ -531,7 +531,7 @@ export const scimServiceFactory = ({
firstName: scimUser.name.givenName, firstName: scimUser.name.givenName,
email: scimUser.emails[0].value, email: scimUser.emails[0].value,
lastName: scimUser.name.familyName, lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : true isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
}, },
tx tx
); );
@@ -790,6 +790,21 @@ export const scimServiceFactory = ({
}); });
const newGroup = await groupDAL.transaction(async (tx) => { 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( const group = await groupDAL.create(
{ {
name: displayName, name: displayName,

View File

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

View File

@@ -36,7 +36,7 @@ export const sendApprovalEmailsFn = async ({
firstName: reviewerUser.firstName, firstName: reviewerUser.firstName,
projectName: project.name, projectName: project.name,
organizationName: project.organization.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 template: SmtpTemplates.SecretApprovalRequestNeedsReview
}); });

View File

@@ -256,6 +256,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`, `${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
db.ref("id").withSchema("secVerTag") db.ref("id").withSchema("secVerTag")
) )
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2)) .select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({ .select({
secVerTagId: "secVerTag.id", secVerTagId: "secVerTag.id",
@@ -279,6 +280,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"), db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"), db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment") 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({ const formatedDoc = sqlNestRelationships({
data: doc, 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 }) => ({ return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el, ...el,
secret: secret?.[0], secret: secret?.[0],

View File

@@ -1,8 +1,8 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { import {
ActionProjectType,
ProjectMembershipRole, ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo, SecretEncryptionAlgo,
SecretKeyEncoding, SecretKeyEncoding,
SecretType, SecretType,
@@ -22,6 +22,8 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; 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 { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { import {
decryptSecretWithBot, decryptSecretWithBot,
@@ -91,6 +93,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">; secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">; userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
@@ -138,18 +141,20 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionV2BridgeDAL, secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
licenseService, licenseService,
projectSlackConfigDAL projectSlackConfigDAL,
resourceMetadataDAL
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
await permissionService.getProjectPermission( await permissionService.getProjectPermission({
actor as ActorType.USER, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId); const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
return count; return count;
@@ -169,7 +174,14 @@ export const secretApprovalRequestServiceFactory = ({
}: TListApprovalsDTO) => { }: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); 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); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
@@ -212,13 +224,14 @@ export const secretApprovalRequestServiceFactory = ({
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const { policy } = secretApprovalRequest; const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerUserId !== actorId && secretApprovalRequest.committerUserId !== actorId &&
@@ -241,6 +254,7 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key, secretKey: el.key,
id: el.id, id: el.id,
version: el.version, version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
@@ -269,7 +283,8 @@ export const secretApprovalRequestServiceFactory = ({
secretComment: el.secretVersion.encryptedComment secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: "", : "",
tags: el.secretVersion.tags tags: el.secretVersion.tags,
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO
} }
: undefined : undefined
})); }));
@@ -330,13 +345,14 @@ export const secretApprovalRequestServiceFactory = ({
}); });
} }
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission({
ActorType.USER, actor: ActorType.USER,
actorId, actorId,
secretApprovalRequest.projectId, projectId: secretApprovalRequest.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerUserId !== actorId && secretApprovalRequest.committerUserId !== actorId &&
@@ -396,13 +412,14 @@ export const secretApprovalRequestServiceFactory = ({
}); });
} }
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission({
ActorType.USER, actor: ActorType.USER,
actorId, actorId,
secretApprovalRequest.projectId, projectId: secretApprovalRequest.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerUserId !== actorId && secretApprovalRequest.committerUserId !== actorId &&
@@ -452,13 +469,14 @@ export const secretApprovalRequestServiceFactory = ({
}); });
} }
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission({
ActorType.USER, actor: ActorType.USER,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
@@ -543,6 +561,7 @@ export const secretApprovalRequestServiceFactory = ({
? await fnSecretV2BridgeBulkInsert({ ? await fnSecretV2BridgeBulkInsert({
tx, tx,
folderId, folderId,
orgId: actorOrgId,
inputSecrets: secretCreationCommits.map((el) => ({ inputSecrets: secretCreationCommits.map((el) => ({
tagIds: el?.tags.map(({ id }) => id), tagIds: el?.tags.map(({ id }) => id),
version: 1, version: 1,
@@ -550,6 +569,7 @@ export const secretApprovalRequestServiceFactory = ({
encryptedValue: el.encryptedValue, encryptedValue: el.encryptedValue,
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key, key: el.key,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
references: el.encryptedValue references: el.encryptedValue
? getAllSecretReferencesV2Bridge( ? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({ secretManagerDecryptor({
@@ -559,6 +579,7 @@ export const secretApprovalRequestServiceFactory = ({
: [], : [],
type: SecretType.Shared type: SecretType.Shared
})), })),
resourceMetadataDAL,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL, secretTagDAL,
@@ -568,6 +589,7 @@ export const secretApprovalRequestServiceFactory = ({
const updatedSecrets = secretUpdationCommits.length const updatedSecrets = secretUpdationCommits.length
? await fnSecretV2BridgeBulkUpdate({ ? await fnSecretV2BridgeBulkUpdate({
folderId, folderId,
orgId: actorOrgId,
tx, tx,
inputSecrets: secretUpdationCommits.map((el) => { inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue = const encryptedValue =
@@ -592,6 +614,7 @@ export const secretApprovalRequestServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key, key: el.key,
tags: el?.tags.map(({ id }) => id), tags: el?.tags.map(({ id }) => id),
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
...encryptedValue ...encryptedValue
} }
}; };
@@ -599,7 +622,8 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL secretVersionTagDAL: secretVersionTagV2BridgeDAL,
resourceMetadataDAL
}) })
: []; : [];
const deletedSecret = secretDeletionCommits.length const deletedSecret = secretDeletionCommits.length
@@ -824,6 +848,7 @@ export const secretApprovalRequestServiceFactory = ({
} }
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
projectId, projectId,
orgId: actorOrgId,
secretPath: folder.path, secretPath: folder.path,
environmentSlug: folder.environmentSlug, environmentSlug: folder.environmentSlug,
actorId, actorId,
@@ -852,7 +877,7 @@ export const secretApprovalRequestServiceFactory = ({
bypassReason, bypassReason,
secretPath: policy.secretPath, secretPath: policy.secretPath,
environment: env.name, environment: env.name,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval` approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`
}, },
template: SmtpTemplates.AccessSecretRequestBypassed template: SmtpTemplates.AccessSecretRequestBypassed
}); });
@@ -876,14 +901,14 @@ export const secretApprovalRequestServiceFactory = ({
}: TGenerateSecretApprovalRequestDTO) => { }: TGenerateSecretApprovalRequestDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
ForbidOnInvalidProjectType(ProjectType.SecretManager); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@@ -1157,14 +1182,14 @@ export const secretApprovalRequestServiceFactory = ({
if (actor === ActorType.SERVICE || actor === ActorType.Machine) if (actor === ActorType.SERVICE || actor === ActorType.Machine)
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" }); throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
ForbidOnInvalidProjectType(ProjectType.SecretManager); });
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
throw new NotFoundError({ throw new NotFoundError({
@@ -1208,6 +1233,7 @@ export const secretApprovalRequestServiceFactory = ({
), ),
skipMultilineEncoding: createdSecret.skipMultilineEncoding, skipMultilineEncoding: createdSecret.skipMultilineEncoding,
key: createdSecret.secretKey, key: createdSecret.secretKey,
secretMetadata: createdSecret.secretMetadata,
type: SecretType.Shared type: SecretType.Shared
})) }))
); );
@@ -1241,9 +1267,10 @@ export const secretApprovalRequestServiceFactory = ({
type: SecretType.Shared type: SecretType.Shared
})) }))
); );
if (secrets.length)
if (secrets.length !== secretsWithNewName.length)
throw new NotFoundError({ 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(",")}`
}); });
} }
@@ -1263,12 +1290,14 @@ export const secretApprovalRequestServiceFactory = ({
reminderNote, reminderNote,
secretComment, secretComment,
metadata, metadata,
skipMultilineEncoding skipMultilineEncoding,
secretMetadata
}) => { }) => {
const secretId = updatingSecretsGroupByKey[secretKey][0].id; const secretId = updatingSecretsGroupByKey[secretKey][0].id;
if (tagIds?.length) commitTagIds[secretKey] = tagIds; if (tagIds?.length) commitTagIds[secretKey] = tagIds;
return { return {
...latestSecretVersions[secretId], ...latestSecretVersions[secretId],
secretMetadata,
key: newSecretName || secretKey, key: newSecretName || secretKey,
encryptedComment: setKnexStringValue( encryptedComment: setKnexStringValue(
secretComment, secretComment,
@@ -1370,7 +1399,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays, reminderRepeatDays,
encryptedValue, encryptedValue,
secretId, secretId,
secretVersion secretVersion,
secretMetadata
}) => ({ }) => ({
version, version,
requestId: doc.id, requestId: doc.id,
@@ -1383,7 +1413,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays, reminderRepeatDays,
reminderNote, reminderNote,
encryptedComment, encryptedComment,
key key,
secretMetadata: JSON.stringify(secretMetadata)
}) })
), ),
tx tx

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
import { Knex } from "knex"; import knex, { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretScanningGitRisksInsert } from "@app/db/schemas"; import { TableName, TSecretScanningGitRisksInsert } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; 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>; 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 };
}; };

View File

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

View File

@@ -1,4 +1,4 @@
import { TOrgPermission } from "@app/lib/types"; import { OrderByDirection, TOrgPermission } from "@app/lib/types";
export enum SecretScanningRiskStatus { export enum SecretScanningRiskStatus {
FalsePositive = "RESOLVED_FALSE_POSITIVE", FalsePositive = "RESOLVED_FALSE_POSITIVE",
@@ -7,6 +7,12 @@ export enum SecretScanningRiskStatus {
Unresolved = "UNRESOLVED" Unresolved = "UNRESOLVED"
} }
export enum SecretScanningResolvedStatus {
All = "all",
Resolved = "resolved",
Unresolved = "unresolved"
}
export type TInstallAppSessionDTO = TOrgPermission; export type TInstallAppSessionDTO = TOrgPermission;
export type TLinkInstallSessionDTO = { export type TLinkInstallSessionDTO = {
@@ -16,7 +22,22 @@ export type TLinkInstallSessionDTO = {
export type TGetOrgInstallStatusDTO = TOrgPermission; 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 = { export type TUpdateRiskStatusDTO = {
riskId: string; riskId: string;

View File

@@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors"; import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
@@ -83,13 +83,14 @@ export const secretSnapshotServiceFactory = ({
actorAuthMethod, actorAuthMethod,
path path
}: TProjectSnapshotCountDTO) => { }: TProjectSnapshotCountDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); 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. // 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, limit = 20,
offset = 0 offset = 0
}: TProjectSnapshotListDTO) => { }: TProjectSnapshotListDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); 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. // 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 getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
const snapshot = await snapshotDAL.findById(id); const snapshot = await snapshotDAL.findById(id);
if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${id}' not found` }); if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
snapshot.projectId, projectId: snapshot.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3; const shouldUseBridge = snapshot.projectVersion === 3;
@@ -322,14 +325,14 @@ export const secretSnapshotServiceFactory = ({
if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` }); if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` });
const shouldUseBridge = snapshot.projectVersion === 3; const shouldUseBridge = snapshot.projectVersion === 3;
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
snapshot.projectId, projectId: snapshot.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SecretManager
ForbidOnInvalidProjectType(ProjectType.SecretManager); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { ProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -54,15 +54,15 @@ export const sshCertificateTemplateServiceFactory = ({
}); });
} }
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
ca.projectId, projectId: ca.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SshCertificateTemplates ProjectPermissionSub.SshCertificateTemplates
@@ -127,15 +127,15 @@ export const sshCertificateTemplateServiceFactory = ({
}); });
} }
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
certTemplate.projectId, projectId: certTemplate.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
ProjectPermissionSub.SshCertificateTemplates ProjectPermissionSub.SshCertificateTemplates
@@ -196,15 +196,15 @@ export const sshCertificateTemplateServiceFactory = ({
}); });
} }
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
certificateTemplate.projectId, projectId: certificateTemplate.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.SshCertificateTemplates ProjectPermissionSub.SshCertificateTemplates
@@ -223,15 +223,15 @@ export const sshCertificateTemplateServiceFactory = ({
}); });
} }
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
certTemplate.projectId, projectId: certTemplate.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
ProjectPermissionSub.SshCertificateTemplates ProjectPermissionSub.SshCertificateTemplates

View File

@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
@@ -65,15 +65,15 @@ export const sshCertificateAuthorityServiceFactory = ({
actor, actor,
actorOrgId actorOrgId
}: TCreateSshCaDTO) => { }: TCreateSshCaDTO) => {
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SshCertificateAuthorities ProjectPermissionSub.SshCertificateAuthorities
@@ -118,15 +118,15 @@ export const sshCertificateAuthorityServiceFactory = ({
const ca = await sshCertificateAuthorityDAL.findById(caId); const ca = await sshCertificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
ca.projectId, projectId: ca.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
ProjectPermissionSub.SshCertificateAuthorities ProjectPermissionSub.SshCertificateAuthorities
@@ -187,15 +187,15 @@ export const sshCertificateAuthorityServiceFactory = ({
const ca = await sshCertificateAuthorityDAL.findById(caId); const ca = await sshCertificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
ca.projectId, projectId: ca.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
ProjectPermissionSub.SshCertificateAuthorities ProjectPermissionSub.SshCertificateAuthorities
@@ -226,15 +226,15 @@ export const sshCertificateAuthorityServiceFactory = ({
const ca = await sshCertificateAuthorityDAL.findById(caId); const ca = await sshCertificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
ca.projectId, projectId: ca.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.SshCertificateAuthorities ProjectPermissionSub.SshCertificateAuthorities
@@ -268,15 +268,15 @@ export const sshCertificateAuthorityServiceFactory = ({
}); });
} }
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
sshCertificateTemplate.projectId, projectId: sshCertificateTemplate.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SshCertificates ProjectPermissionSub.SshCertificates
@@ -390,15 +390,15 @@ export const sshCertificateAuthorityServiceFactory = ({
}); });
} }
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
sshCertificateTemplate.projectId, projectId: sshCertificateTemplate.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SshCertificates ProjectPermissionSub.SshCertificates
@@ -488,15 +488,15 @@ export const sshCertificateAuthorityServiceFactory = ({
const ca = await sshCertificateAuthorityDAL.findById(caId); const ca = await sshCertificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
ca.projectId, projectId: ca.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.SSH
});
ForbidOnInvalidProjectType(ProjectType.SSH);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
ProjectPermissionSub.SshCertificateTemplates ProjectPermissionSub.SshCertificateTemplates

View File

@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
@@ -27,13 +28,14 @@ export const trustedIpServiceFactory = ({
projectDAL projectDAL
}: TTrustedIpServiceFactoryDep) => { }: TTrustedIpServiceFactoryDep) => {
const listIpsByProjectId = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TProjectPermission) => { const listIpsByProjectId = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
const trustedIps = await trustedIpDAL.find({ const trustedIps = await trustedIpDAL.find({
projectId projectId
@@ -51,13 +53,14 @@ export const trustedIpServiceFactory = ({
comment, comment,
isActive isActive
}: TCreateIpDTO) => { }: TCreateIpDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@@ -96,13 +99,14 @@ export const trustedIpServiceFactory = ({
comment, comment,
trustedIpId trustedIpId
}: TUpdateIpDTO) => { }: TUpdateIpDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@@ -141,13 +145,14 @@ export const trustedIpServiceFactory = ({
actorAuthMethod, actorAuthMethod,
trustedIpId trustedIpId
}: TDeleteIpDTO) => { }: TDeleteIpDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);

View File

@@ -23,6 +23,8 @@ export const KeyStorePrefixes = {
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const, `sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const, `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) => IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`, `identity-access-token-status:${identityAccessTokenId}`,
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}` ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
@@ -30,6 +32,7 @@ export const KeyStorePrefixes = {
export const KeyStoreTtls = { export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60, SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
SetSecretSyncLastRunTimestampInSeconds: 60,
AccessTokenStatusUpdateInSeconds: 120 AccessTokenStatusUpdateInSeconds: 120
}; };

View File

@@ -1,5 +1,7 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
export const GROUPS = { export const GROUPS = {
CREATE: { CREATE: {
@@ -474,7 +476,7 @@ export const PROJECTS = {
}, },
ADD_GROUP_TO_PROJECT: { ADD_GROUP_TO_PROJECT: {
projectId: "The ID of the project to add the group to.", projectId: "The ID of the project to add the group to.",
groupId: "The ID of the group to add to the project.", groupIdOrName: "The ID or name of the group to add to the project.",
role: "The role for the group to assume in the project." role: "The role for the group to assume in the project."
}, },
UPDATE_GROUP_IN_PROJECT: { UPDATE_GROUP_IN_PROJECT: {
@@ -741,6 +743,12 @@ export const RAW_SECRETS = {
workspaceId: "The ID of the project where the secret is located.", workspaceId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.", environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located." secretPath: "The folder path where the secret is located."
},
GET_ACCESS_LIST: {
secretName: "The name of the secret to get the access list for.",
workspaceId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located."
} }
} as const; } as const;
@@ -820,6 +828,8 @@ export const AUDIT_LOGS = {
projectId: projectId:
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", "Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
eventType: "The type of the event to export.", eventType: "The type of the event to export.",
secretPath:
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
userAgentType: "Choose which consuming application to export audit logs for.", userAgentType: "Choose which consuming application to export audit logs for.",
eventMetadata: eventMetadata:
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.", "Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
@@ -1150,7 +1160,8 @@ export const INTEGRATION = {
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.", shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.", shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
shouldEnableDelete: "The flag to enable deletion of secrets.", shouldEnableDelete: "The flag to enable deletion of secrets.",
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy." octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy.",
metadataSyncMode: "The mode for syncing metadata to external system"
} }
}, },
UPDATE: { UPDATE: {
@@ -1636,6 +1647,83 @@ export const AppConnections = {
}; };
}, },
DELETE: (app: AppConnection) => ({ DELETE: (app: AppConnection) => ({
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.` connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
}) })
}; };
export const SecretSyncs = {
LIST: (destination?: SecretSync) => ({
projectId: `The ID of the project to list ${destination ? SECRET_SYNC_NAME_MAP[destination] : "Secret"} Syncs from.`
}),
GET_BY_ID: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.`
}),
GET_BY_NAME: (destination: SecretSync) => ({
syncName: `The name of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.`,
projectId: `The ID of the project the ${SECRET_SYNC_NAME_MAP[destination]} Sync is associated with.`
}),
CREATE: (destination: SecretSync) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
name: `The name of the ${destinationName} Sync to create. Must be slug-friendly.`,
description: `An optional description for the ${destinationName} Sync.`,
projectId: "The ID of the project to create the sync in.",
environment: `The slug of the project environment to sync secrets from.`,
secretPath: `The folder path to sync secrets from.`,
connectionId: `The ID of the ${
APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]]
} Connection to use for syncing.`,
isAutoSyncEnabled: `Whether secrets should be automatically synced when changes occur at the source location or not.`,
syncOptions: "Optional parameters to modify how secrets are synced."
};
},
UPDATE: (destination: SecretSync) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
syncId: `The ID of the ${destinationName} Sync to be updated.`,
connectionId: `The updated ID of the ${
APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]]
} Connection to use for syncing.`,
name: `The updated name of the ${destinationName} Sync. Must be slug-friendly.`,
environment: `The updated slug of the project environment to sync secrets from.`,
secretPath: `The updated folder path to sync secrets from.`,
description: `The updated description of the ${destinationName} Sync.`,
isAutoSyncEnabled: `Whether secrets should be automatically synced when changes occur at the source location or not.`,
syncOptions: "Optional parameters to modify how secrets are synced."
};
},
DELETE: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to be deleted.`,
removeSecrets: `Whether previously synced secrets should be removed prior to deletion.`
}),
SYNC_SECRETS: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger a sync for.`
}),
IMPORT_SECRETS: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger importing secrets for.`,
importBehavior: `Specify whether Infisical should prioritize secret values from Infisical or ${SECRET_SYNC_NAME_MAP[destination]}.`
}),
REMOVE_SECRETS: (destination: SecretSync) => ({
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger removing secrets for.`
}),
SYNC_OPTIONS: (destination: SecretSync) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
INITIAL_SYNC_BEHAVIOR: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
PREPEND_PREFIX: `Optionally prepend a prefix to your secrets' keys when syncing to ${destinationName}.`,
APPEND_SUFFIX: `Optionally append a suffix to your secrets' keys when syncing to ${destinationName}.`
};
},
DESTINATION_CONFIG: {
AWS_PARAMETER_STORE: {
REGION: "The AWS region to sync secrets to.",
PATH: "The Parameter Store path to sync secrets to."
},
GITHUB: {
ORG: "The name of the GitHub organization.",
OWNER: "The name of the GitHub account owner of the repository.",
REPO: "The name of the GitHub repository.",
ENV: "The name of the GitHub environment."
}
}
};

View File

@@ -157,6 +157,8 @@ const envSchema = z
INFISICAL_CLOUD: zodStrBool.default("false"), INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false"), MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional()), CAPTCHA_SECRET: zpStr(z.string().optional()),
CAPTCHA_SITE_KEY: zpStr(z.string().optional()),
INTERCOM_ID: zpStr(z.string().optional()),
// TELEMETRY // TELEMETRY
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"), OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
@@ -197,7 +199,29 @@ const envSchema = z
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()), INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()), INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()), INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()) INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
/* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr(
z
.string()
.optional()
.transform((val) => {
if (!val) return undefined;
return JSON.parse(val) as string[];
})
),
CORS_ALLOWED_HEADERS: zpStr(
z
.string()
.optional()
.transform((val) => {
if (!val) return undefined;
return JSON.parse(val) as string[];
})
)
}) })
// To ensure that basic encryption is always possible. // To ensure that basic encryption is always possible.
.refine( .refine(

View File

@@ -1,3 +1,4 @@
export { isDisposableEmail } from "./validate-email"; export { isDisposableEmail } from "./validate-email";
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name"; export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
export { blockLocalAndPrivateIpAddresses } from "./validate-url"; export { blockLocalAndPrivateIpAddresses } from "./validate-url";
export { isUuidV4 } from "./validate-uuid";

View File

@@ -0,0 +1,3 @@
import { z } from "zod";
export const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;

View File

@@ -15,6 +15,12 @@ import {
TIntegrationSyncPayload, TIntegrationSyncPayload,
TSyncSecretsDTO TSyncSecretsDTO
} from "@app/services/secret/secret-types"; } from "@app/services/secret/secret-types";
import {
TQueueSecretSyncImportSecretsByIdDTO,
TQueueSecretSyncRemoveSecretsByIdDTO,
TQueueSecretSyncSyncSecretsByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types";
export enum QueueName { export enum QueueName {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@@ -36,7 +42,8 @@ export enum QueueName {
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update", AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source" ImportSecretsFromExternalSource = "import-secrets-from-external-source",
AppConnectionSecretSync = "app-connection-secret-sync"
} }
export enum QueueJobs { export enum QueueJobs {
@@ -61,7 +68,11 @@ export enum QueueJobs {
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update", IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update", ServiceTokenStatusUpdate = "service-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source" ImportSecretsFromExternalSource = "import-secrets-from-external-source",
SecretSyncSyncSecrets = "secret-sync-sync-secrets",
SecretSyncImportSecrets = "secret-sync-import-secrets",
SecretSyncRemoveSecrets = "secret-sync-remove-secrets",
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -184,6 +195,23 @@ export type TQueueJobTypes = {
}; };
}; };
}; };
[QueueName.AppConnectionSecretSync]:
| {
name: QueueJobs.SecretSyncSyncSecrets;
payload: TQueueSecretSyncSyncSecretsByIdDTO;
}
| {
name: QueueJobs.SecretSyncImportSecrets;
payload: TQueueSecretSyncImportSecretsByIdDTO;
}
| {
name: QueueJobs.SecretSyncRemoveSecrets;
payload: TQueueSecretSyncRemoveSecretsByIdDTO;
}
| {
name: QueueJobs.SecretSyncSendActionFailedNotifications;
payload: TQueueSendSecretSyncActionFailedNotificationsDTO;
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -27,10 +27,10 @@ import { globalRateLimiterCfg } from "./config/rateLimiter";
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas"; import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
import { apiMetrics } from "./plugins/api-metrics"; import { apiMetrics } from "./plugins/api-metrics";
import { fastifyErrHandler } from "./plugins/error-handler"; import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod"; import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
import { fastifyIp } from "./plugins/ip"; import { fastifyIp } from "./plugins/ip";
import { maintenanceMode } from "./plugins/maintenanceMode"; import { maintenanceMode } from "./plugins/maintenanceMode";
import { registerServeUI } from "./plugins/serve-ui";
import { fastifySwagger } from "./plugins/swagger"; import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes"; import { registerRoutes } from "./routes";
@@ -87,7 +87,16 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register<FastifyCorsOptions>(cors, { await server.register<FastifyCorsOptions>(cors, {
credentials: true, credentials: true,
origin: appCfg.SITE_URL || true ...(appCfg.CORS_ALLOWED_ORIGINS?.length
? {
origin: [...appCfg.CORS_ALLOWED_ORIGINS, ...(appCfg.SITE_URL ? [appCfg.SITE_URL] : [])]
}
: {
origin: appCfg.SITE_URL || true
}),
...(appCfg.CORS_ALLOWED_HEADERS?.length && {
allowedHeaders: appCfg.CORS_ALLOWED_HEADERS
})
}); });
await server.register(addErrorsToResponseSchemas); await server.register(addErrorsToResponseSchemas);
@@ -120,13 +129,10 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule }); await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
if (appCfg.isProductionMode) { await server.register(registerServeUI, {
await server.register(registerExternalNextjs, { standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED, dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../")
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../"), });
port: appCfg.PORT
});
}
await server.ready(); await server.ready();
server.swagger(); server.swagger();

View File

@@ -32,13 +32,21 @@ export const getUserAgentType = (userAgent: string | undefined) => {
export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => { export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auditLogInfo", null); server.decorateRequest("auditLogInfo", null);
server.addHook("onRequest", async (req) => { server.addHook("onRequest", async (req) => {
if (!req.auth) return;
const userAgent = req.headers["user-agent"] ?? ""; const userAgent = req.headers["user-agent"] ?? "";
const payload = { const payload = {
ipAddress: req.realIp, ipAddress: req.realIp,
userAgent, userAgent,
userAgentType: getUserAgentType(userAgent) userAgentType: getUserAgentType(userAgent)
} as typeof req.auditLogInfo; } as typeof req.auditLogInfo;
if (!req.auth) {
payload.actor = {
type: ActorType.UNKNOWN_USER,
metadata: {}
};
req.auditLogInfo = payload;
return;
}
if (req.auth.actor === ActorType.USER) { if (req.auth.actor === ActorType.USER) {
payload.actor = { payload.actor = {
type: ActorType.USER, type: ActorType.USER,

View File

@@ -1,76 +0,0 @@
// this plugins allows to run infisical in standalone mode
// standalone mode = infisical backend and nextjs frontend in one server
// this way users don't need to deploy two things
import path from "node:path";
import { IS_PACKAGED } from "@app/lib/config/env";
// to enabled this u need to set standalone mode to true
export const registerExternalNextjs = async (
server: FastifyZodProvider,
{
standaloneMode,
dir,
port
}: {
standaloneMode?: boolean;
dir: string;
port: number;
}
) => {
if (standaloneMode) {
const frontendName = IS_PACKAGED ? "frontend" : "frontend-build";
const nextJsBuildPath = path.join(dir, frontendName);
const { default: conf } = (await import(
path.join(dir, `${frontendName}/.next/required-server-files.json`),
// @ts-expect-error type
{
assert: { type: "json" }
}
)) as { default: { config: string } };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let NextServer: any;
if (!IS_PACKAGED) {
/* eslint-disable */
const { default: nextServer } = (
await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`))
).default;
NextServer = nextServer;
} else {
/* eslint-disable */
const nextServer = await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`));
NextServer = nextServer.default;
}
const nextApp = new NextServer({
dev: false,
dir: nextJsBuildPath,
port,
conf: conf.config,
hostname: "local",
customServer: false
});
server.route({
method: ["GET", "PUT", "PATCH", "POST", "DELETE"],
url: "/*",
schema: {
hide: true
},
handler: (req, res) =>
nextApp
.getRequestHandler()(req.raw, res.raw)
.then(() => {
res.hijack();
})
});
server.addHook("onClose", () => nextApp.close());
await nextApp.prepare();
/* eslint-enable */
}
};

View File

@@ -1,3 +1,4 @@
import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types";
import { PushEvent } from "@octokit/webhooks-types"; import { PushEvent } from "@octokit/webhooks-types";
import { Probot } from "probot"; import { Probot } from "probot";
import SmeeClient from "smee-client"; import SmeeClient from "smee-client";
@@ -54,14 +55,14 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
handler: async (req, res) => { handler: async (req, res) => {
const eventName = req.headers["x-github-event"]; const eventName = req.headers["x-github-event"] as EmitterWebhookEventName;
const signatureSHA256 = req.headers["x-hub-signature-256"] as string; const signatureSHA256 = req.headers["x-hub-signature-256"] as string;
const id = req.headers["x-github-delivery"] as string; const id = req.headers["x-github-delivery"] as string;
await probot.webhooks.verifyAndReceive({ await probot.webhooks.verifyAndReceive({
id, id,
// @ts-expect-error type
name: eventName, name: eventName,
payload: req.body as string, payload: JSON.stringify(req.body),
signature: signatureSHA256 signature: signatureSHA256
}); });
void res.send("ok"); void res.send("ok");

View File

@@ -0,0 +1,64 @@
import path from "node:path";
import staticServe from "@fastify/static";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
// to enabled this u need to set standalone mode to true
export const registerServeUI = async (
server: FastifyZodProvider,
{
standaloneMode,
dir
}: {
standaloneMode?: boolean;
dir: string;
}
) => {
// use this only for frontend runtime static non-sensitive configuration in standalone mode
// that app needs before loading like posthog dsn key
// for most of the other usecase use server config
server.route({
method: "GET",
url: "/runtime-ui-env.js",
schema: {
hide: true
},
handler: (_req, res) => {
const appCfg = getConfig();
void res.type("application/javascript");
const config = {
CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY,
POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY,
INTERCOM_ID: appCfg.INTERCOM_ID,
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED
};
const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`;
void res.send(js);
}
});
if (standaloneMode) {
const frontendName = IS_PACKAGED ? "frontend" : "frontend-build";
const frontendPath = path.join(dir, frontendName);
await server.register(staticServe, {
root: frontendPath,
wildcard: false
});
server.route({
method: "GET",
url: "/*",
schema: {
hide: true
},
handler: (request, reply) => {
if (request.url.startsWith("/api")) {
reply.callNotFound();
return;
}
void reply.sendFile("index.html");
}
});
}
};

View File

@@ -181,6 +181,7 @@ import { projectUserMembershipRoleDALFactory } from "@app/services/project-membe
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { secretDALFactory } from "@app/services/secret/secret-dal"; import { secretDALFactory } from "@app/services/secret/secret-dal";
import { secretQueueFactory } from "@app/services/secret/secret-queue"; import { secretQueueFactory } from "@app/services/secret/secret-queue";
import { secretServiceFactory } from "@app/services/secret/secret-service"; import { secretServiceFactory } from "@app/services/secret/secret-service";
@@ -195,6 +196,9 @@ import { secretImportDALFactory } from "@app/services/secret-import/secret-impor
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service"; import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal"; import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service"; import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { secretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
import { secretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-queue";
import { secretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service";
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service"; import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal"; import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
@@ -317,6 +321,7 @@ export const registerRoutes = async (
const trustedIpDAL = trustedIpDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db);
const telemetryDAL = telemetryDALFactory(db); const telemetryDAL = telemetryDALFactory(db);
const appConnectionDAL = appConnectionDALFactory(db); const appConnectionDAL = appConnectionDALFactory(db);
const secretSyncDAL = secretSyncDALFactory(db, folderDAL);
// ee db layer ops // ee db layer ops
const permissionDAL = permissionDALFactory(db); const permissionDAL = permissionDALFactory(db);
@@ -374,6 +379,7 @@ export const registerRoutes = async (
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db); const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
const projectTemplateDAL = projectTemplateDALFactory(db); const projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@@ -606,6 +612,7 @@ export const registerRoutes = async (
}); });
const superAdminService = superAdminServiceFactory({ const superAdminService = superAdminServiceFactory({
userDAL, userDAL,
userAliasDAL,
authService: loginService, authService: loginService,
serverCfgDAL: superAdminDAL, serverCfgDAL: superAdminDAL,
kmsRootConfigDAL, kmsRootConfigDAL,
@@ -821,6 +828,29 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const secretSyncQueue = secretSyncQueueFactory({
queueService,
secretSyncDAL,
folderDAL,
secretImportDAL,
secretV2BridgeDAL,
kmsService,
keyStore,
auditLogService,
smtpService,
projectDAL,
projectMembershipDAL,
projectBotDAL,
secretDAL,
secretBlindIndexDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
});
const secretQueueService = secretQueueFactory({ const secretQueueService = secretQueueFactory({
keyStore, keyStore,
queueService, queueService,
@@ -854,7 +884,9 @@ export const registerRoutes = async (
secretApprovalRequestDAL, secretApprovalRequestDAL,
projectKeyDAL, projectKeyDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
orgService orgService,
resourceMetadataDAL,
secretSyncQueue
}); });
const projectService = projectServiceFactory({ const projectService = projectServiceFactory({
@@ -891,7 +923,8 @@ export const registerRoutes = async (
certificateTemplateDAL, certificateTemplateDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
projectTemplateService projectTemplateService,
groupProjectDAL
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -980,7 +1013,8 @@ export const registerRoutes = async (
secretApprovalPolicyService, secretApprovalPolicyService,
secretApprovalRequestSecretDAL, secretApprovalRequestSecretDAL,
kmsService, kmsService,
snapshotService snapshotService,
resourceMetadataDAL
}); });
const secretApprovalRequestService = secretApprovalRequestServiceFactory({ const secretApprovalRequestService = secretApprovalRequestServiceFactory({
@@ -1007,7 +1041,8 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
userDAL, userDAL,
licenseService, licenseService,
projectSlackConfigDAL projectSlackConfigDAL,
resourceMetadataDAL
}); });
const secretService = secretServiceFactory({ const secretService = secretServiceFactory({
@@ -1028,7 +1063,8 @@ export const registerRoutes = async (
secretApprovalRequestDAL, secretApprovalRequestDAL,
secretApprovalRequestSecretDAL, secretApprovalRequestSecretDAL,
secretV2BridgeService, secretV2BridgeService,
secretApprovalRequestService secretApprovalRequestService,
licenseService
}); });
const secretSharingService = secretSharingServiceFactory({ const secretSharingService = secretSharingServiceFactory({
@@ -1086,8 +1122,10 @@ export const registerRoutes = async (
kmsService, kmsService,
secretV2BridgeDAL, secretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL, secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL secretVersionV2BridgeDAL,
resourceMetadataDAL
}); });
const secretRotationQueue = secretRotationQueueFactory({ const secretRotationQueue = secretRotationQueueFactory({
telemetryService, telemetryService,
secretRotationDAL, secretRotationDAL,
@@ -1339,7 +1377,8 @@ export const registerRoutes = async (
folderDAL, folderDAL,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
queueService, queueService,
secretV2BridgeService secretV2BridgeService,
resourceMetadataDAL
}); });
const migrationService = externalMigrationServiceFactory({ const migrationService = externalMigrationServiceFactory({
@@ -1358,8 +1397,17 @@ export const registerRoutes = async (
const appConnectionService = appConnectionServiceFactory({ const appConnectionService = appConnectionServiceFactory({
appConnectionDAL, appConnectionDAL,
permissionService, permissionService,
kmsService, kmsService
licenseService });
const secretSyncService = secretSyncServiceFactory({
secretSyncDAL,
permissionService,
appConnectionService,
folderDAL,
secretSyncQueue,
projectBotService,
keyStore
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -1459,7 +1507,8 @@ export const registerRoutes = async (
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService, externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
projectTemplate: projectTemplateService, projectTemplate: projectTemplateService,
totp: totpService, totp: totpService,
appConnection: appConnectionService appConnection: appConnectionService,
secretSync: secretSyncService
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];

View File

@@ -15,7 +15,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
app, app,
createSchema, createSchema,
updateSchema, updateSchema,
responseSchema sanitizedResponseSchema
}: { }: {
app: AppConnection; app: AppConnection;
server: FastifyZodProvider; server: FastifyZodProvider;
@@ -26,7 +26,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
description?: string | null; description?: string | null;
}>; }>;
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>; updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
responseSchema: z.ZodTypeAny; sanitizedResponseSchema: z.ZodTypeAny;
}) => { }) => {
const appName = APP_CONNECTION_NAME_MAP[app]; const appName = APP_CONNECTION_NAME_MAP[app];
@@ -39,7 +39,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: { schema: {
description: `List the ${appName} Connections for the current organization.`, description: `List the ${appName} Connections for the current organization.`,
response: { response: {
200: z.object({ appConnections: responseSchema.array() }) 200: z.object({ appConnections: sanitizedResponseSchema.array() })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -63,6 +63,44 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
} }
}); });
server.route({
method: "GET",
url: "/available",
config: {
rateLimit: readLimit
},
schema: {
description: `List the ${appName} Connections the current user has permission to establish connections with.`,
response: {
200: z.object({
appConnections: z.object({ app: z.literal(app), name: z.string(), id: z.string().uuid() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser(
app,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS,
metadata: {
app,
count: appConnections.length,
connectionIds: appConnections.map((connection) => connection.id)
}
}
});
return { appConnections };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:connectionId", url: "/:connectionId",
@@ -75,7 +113,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId) connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
}), }),
response: { response: {
200: z.object({ appConnection: responseSchema }) 200: z.object({ appConnection: sanitizedResponseSchema })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -105,7 +143,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
server.route({ server.route({
method: "GET", method: "GET",
url: `/name/:connectionName`, url: `/connection-name/:connectionName`,
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -114,11 +152,12 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
params: z.object({ params: z.object({
connectionName: z connectionName: z
.string() .string()
.min(0, "Connection name required") .trim()
.min(1, "Connection name required")
.describe(AppConnections.GET_BY_NAME(app).connectionName) .describe(AppConnections.GET_BY_NAME(app).connectionName)
}), }),
response: { response: {
200: z.object({ appConnection: responseSchema }) 200: z.object({ appConnection: sanitizedResponseSchema })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -158,7 +197,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
} ${appName} Connection for the current organization.`, } ${appName} Connection for the current organization.`,
body: createSchema, body: createSchema,
response: { response: {
200: z.object({ appConnection: responseSchema }) 200: z.object({ appConnection: sanitizedResponseSchema })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -168,7 +207,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
const appConnection = (await server.services.appConnection.createAppConnection( const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description }, { name, method, app, credentials, description },
req.permission req.permission
)) as TAppConnection; )) as T;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -201,7 +240,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
}), }),
body: updateSchema, body: updateSchema,
response: { response: {
200: z.object({ appConnection: responseSchema }) 200: z.object({ appConnection: sanitizedResponseSchema })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -244,7 +283,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId) connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
}), }),
response: { response: {
200: z.object({ appConnection: responseSchema }) 200: z.object({ appConnection: sanitizedResponseSchema })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),

View File

@@ -1,17 +0,0 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateGitHubConnectionSchema,
SanitizedGitHubConnectionSchema,
UpdateGitHubConnectionSchema
} from "@app/services/app-connection/github";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) =>
registerAppConnectionEndpoints({
app: AppConnection.GitHub,
server,
responseSchema: SanitizedGitHubConnectionSchema,
createSchema: CreateGitHubConnectionSchema,
updateSchema: UpdateGitHubConnectionSchema
});

View File

@@ -1,8 +0,0 @@
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
[AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter
};

View File

@@ -11,7 +11,7 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
registerAppConnectionEndpoints({ registerAppConnectionEndpoints({
app: AppConnection.AWS, app: AppConnection.AWS,
server, server,
responseSchema: SanitizedAwsConnectionSchema, sanitizedResponseSchema: SanitizedAwsConnectionSchema,
createSchema: CreateAwsConnectionSchema, createSchema: CreateAwsConnectionSchema,
updateSchema: UpdateAwsConnectionSchema updateSchema: UpdateAwsConnectionSchema
}); });

View File

@@ -0,0 +1,117 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateGitHubConnectionSchema,
SanitizedGitHubConnectionSchema,
UpdateGitHubConnectionSchema
} from "@app/services/app-connection/github";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.GitHub,
server,
sanitizedResponseSchema: SanitizedGitHubConnectionSchema,
createSchema: CreateGitHubConnectionSchema,
updateSchema: UpdateGitHubConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/repositories`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
repositories: z
.object({ id: z.number(), name: z.string(), owner: z.object({ login: z.string(), id: z.number() }) })
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
const repositories = await server.services.appConnection.github.listRepositories(connectionId, req.permission);
return { repositories };
}
});
server.route({
method: "GET",
url: `/:connectionId/organizations`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
organizations: z.object({ id: z.number(), login: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
const organizations = await server.services.appConnection.github.listOrganizations(connectionId, req.permission);
return { organizations };
}
});
server.route({
method: "GET",
url: `/:connectionId/environments`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
repo: z.string().min(1, "Repository name is required"),
owner: z.string().min(1, "Repository owner name is required")
}),
response: {
200: z.object({
environments: z.object({ id: z.number(), name: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
const { repo, owner } = req.query;
const environments = await server.services.appConnection.github.listEnvironments(
{
connectionId,
repo,
owner
},
req.permission
);
return { environments };
}
});
};

View File

@@ -1,2 +1,12 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
export * from "./app-connection-router"; export * from "./app-connection-router";
export * from "./apps";
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
{
[AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter
};

View File

@@ -63,7 +63,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
token: z.string() token: z.string(),
organizationId: z.string().optional()
}) })
} }
}, },
@@ -115,7 +116,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
{ expiresIn: appCfg.JWT_AUTH_LIFETIME } { expiresIn: appCfg.JWT_AUTH_LIFETIME }
); );
return { token }; return { token, organizationId: decodedToken.organizationId };
} }
}); });
}; };

View File

@@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { import {
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -17,6 +17,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -116,6 +117,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
@@ -218,13 +220,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalCount: totalFolderCount ?? 0 totalCount: totalFolderCount ?? 0
}; };
const { permission } = await server.services.permission.getProjectPermission( const { permission } = await server.services.permission.getProjectPermission({
req.permission.type, actor: req.permission.type,
req.permission.id, actorId: req.permission.id,
projectId, projectId,
req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
req.permission.orgId actorOrgId: req.permission.orgId,
); actionProjectType: ActionProjectType.SecretManager
});
const allowedDynamicSecretEnvironments = // filter envs user has access to const allowedDynamicSecretEnvironments = // filter envs user has access to
environments.filter((environment) => environments.filter((environment) =>
@@ -408,6 +411,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
@@ -693,6 +697,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
@@ -864,6 +869,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,

View File

@@ -1,6 +1,10 @@
import { APP_CONNECTION_REGISTER_MAP, registerAppConnectionRouter } from "@app/server/routes/v1/app-connection-routers"; import {
APP_CONNECTION_REGISTER_ROUTER_MAP,
registerAppConnectionRouter
} from "@app/server/routes/v1/app-connection-routers";
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router"; import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"; import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers";
import { registerAdminRouter } from "./admin-router"; import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router"; import { registerAuthRoutes } from "./auth-router";
@@ -113,12 +117,28 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" }); await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
await server.register( await server.register(
async (appConnectionsRouter) => { async (appConnectionRouter) => {
await appConnectionsRouter.register(registerAppConnectionRouter); // register generic app connection endpoints
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_MAP)) { await appConnectionRouter.register(registerAppConnectionRouter);
await appConnectionsRouter.register(router, { prefix: `/${app}` });
// register service specific endpoints (app-connections/aws, app-connections/github, etc.)
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_ROUTER_MAP)) {
await appConnectionRouter.register(router, { prefix: `/${app}` });
} }
}, },
{ prefix: "/app-connections" } { prefix: "/app-connections" }
); );
await server.register(
async (secretSyncRouter) => {
// register generic secret sync endpoints
await secretSyncRouter.register(registerSecretSyncRouter);
// register service specific secret sync endpoints (secret-syncs/aws-parameter-store, secret-syncs/github, etc.)
for await (const [destination, router] of Object.entries(SECRET_SYNC_REGISTER_ROUTER_MAP)) {
await secretSyncRouter.register(router, { prefix: `/${destination}` });
}
},
{ prefix: "/secret-syncs" }
);
}; };

View File

@@ -131,7 +131,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app), app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app),
appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId), appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId),
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive), isActive: z.boolean().optional().describe(INTEGRATION.UPDATE.isActive),
secretPath: z secretPath: z
.string() .string()
.trim() .trim()

View File

@@ -73,6 +73,40 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
url: "/signup-resend",
config: {
rateLimit: inviteUserRateLimit
},
method: "POST",
schema: {
body: z.object({
membershipId: z.string()
}),
response: {
200: z.object({
signupToken: z
.object({
email: z.string(),
link: z.string()
})
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.org.resendOrgMemberInvitation({
orgId: req.permission.orgId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
membershipId: req.body.membershipId
});
}
});
server.route({ server.route({
url: "/verify", url: "/verify",
method: "POST", method: "POST",

View File

@@ -11,7 +11,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn"; import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -113,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
querystring: z.object({ querystring: z.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
actorType: z.nativeEnum(ActorType).optional(), actorType: z.nativeEnum(ActorType).optional(),
secretPath: z
.string()
.optional()
.transform((val) => (!val ? val : removeTrailingSlash(val)))
.describe(AUDIT_LOGS.EXPORT.secretPath),
// eventType is split with , for multiple values, we need to transform it to array // eventType is split with , for multiple values, we need to transform it to array
eventType: z eventType: z
.string() .string()

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSharingSchema } from "@app/db/schemas"; import { SecretSharingSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSharingAccessType } from "@app/lib/types"; import { SecretSharingAccessType } from "@app/lib/types";
import { import {
publicEndpointLimit, publicEndpointLimit,
@@ -88,6 +89,21 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
orgId: req.permission?.orgId orgId: req.permission?.orgId
}); });
if (sharedSecret.secret?.orgId) {
await server.services.auditLog.createAuditLog({
orgId: sharedSecret.secret.orgId,
...req.auditLogInfo,
event: {
type: EventType.READ_SHARED_SECRET,
metadata: {
id: req.params.id,
name: sharedSecret.secret.name || undefined,
accessType: sharedSecret.secret.accessType
}
}
});
}
return sharedSecret; return sharedSecret;
} }
}); });
@@ -151,6 +167,23 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body ...req.body
}); });
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SHARED_SECRET,
metadata: {
accessType: req.body.accessType,
expiresAt: req.body.expiresAt,
expiresAfterViews: req.body.expiresAfterViews,
name: req.body.name,
id: sharedSecret.id,
usingPassword: !!req.body.password
}
}
});
return { id: sharedSecret.id }; return { id: sharedSecret.id };
} }
}); });
@@ -181,6 +214,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
sharedSecretId sharedSecretId
}); });
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_SHARED_SECRET,
metadata: {
id: sharedSecretId,
name: deletedSharedSecret.name || undefined
}
}
});
return { ...deletedSharedSecret }; return { ...deletedSharedSecret };
} }
}); });

View File

@@ -0,0 +1,17 @@
import {
AwsParameterStoreSyncSchema,
CreateAwsParameterStoreSyncSchema,
UpdateAwsParameterStoreSyncSchema
} from "@app/services/secret-sync/aws-parameter-store";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerAwsParameterStoreSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.AWSParameterStore,
server,
responseSchema: AwsParameterStoreSyncSchema,
createSchema: CreateAwsParameterStoreSyncSchema,
updateSchema: UpdateAwsParameterStoreSyncSchema
});

View File

@@ -0,0 +1,13 @@
import { CreateGitHubSyncSchema, GitHubSyncSchema, UpdateGitHubSyncSchema } from "@app/services/secret-sync/github";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerGitHubSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.GitHub,
server,
responseSchema: GitHubSyncSchema,
createSchema: CreateGitHubSyncSchema,
updateSchema: UpdateGitHubSyncSchema
});

View File

@@ -0,0 +1,11 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
export * from "./secret-sync-router";
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
[SecretSync.GitHub]: registerGitHubSyncRouter
};

View File

@@ -0,0 +1,408 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSyncs } from "@app/lib/api-docs";
import { startsWithVowel } from "@app/lib/fn";
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 { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretSync, TSecretSyncInput } from "@app/services/secret-sync/secret-sync-types";
export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TSecretSyncInput>({
server,
destination,
createSchema,
updateSchema,
responseSchema
}: {
destination: SecretSync;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
environment: string;
secretPath: string;
projectId: string;
connectionId: string;
destinationConfig: I["destinationConfig"];
syncOptions: I["syncOptions"];
description?: string | null;
isAutoSyncEnabled?: boolean;
}>;
updateSchema: z.ZodType<{
connectionId?: string;
name?: string;
environment?: string;
secretPath?: string;
destinationConfig?: I["destinationConfig"];
syncOptions?: I["syncOptions"];
description?: string | null;
isAutoSyncEnabled?: boolean;
}>;
responseSchema: z.ZodTypeAny;
}) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
description: `List the ${destinationName} Syncs for the specified project.`,
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST(destination).projectId)
}),
response: {
200: z.object({ secretSyncs: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
} = req;
const secretSyncs = (await server.services.secretSync.listSecretSyncsByProjectId(
{ projectId, destination },
req.permission
)) as T[];
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_SYNCS,
metadata: {
destination,
count: secretSyncs.length,
syncIds: secretSyncs.map((connection) => connection.id)
}
}
});
return { secretSyncs };
}
});
server.route({
method: "GET",
url: "/:syncId",
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${destinationName} Sync by ID.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.GET_BY_ID(destination).syncId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.findSecretSyncById(
{ syncId, destination },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretSync.projectId,
event: {
type: EventType.GET_SECRET_SYNC,
metadata: {
syncId,
destination
}
}
});
return { secretSync };
}
});
server.route({
method: "GET",
url: `/sync-name/:syncName`,
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${destinationName} Sync by name and project ID.`,
params: z.object({
syncName: z.string().trim().min(1, "Sync name required").describe(SecretSyncs.GET_BY_NAME(destination).syncName)
}),
querystring: z.object({
projectId: z
.string()
.trim()
.min(1, "Project ID required")
.describe(SecretSyncs.GET_BY_NAME(destination).projectId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncName } = req.params;
const { projectId } = req.query;
const secretSync = (await server.services.secretSync.findSecretSyncByName(
{ syncName, projectId, destination },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_SYNC,
metadata: {
syncId: secretSync.id,
destination
}
}
});
return { secretSync };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: `Create ${
startsWithVowel(destinationName) ? "an" : "a"
} ${destinationName} Sync for the specified project environment.`,
body: createSchema,
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretSync = (await server.services.secretSync.createSecretSync(
{ ...req.body, destination },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretSync.projectId,
event: {
type: EventType.CREATE_SECRET_SYNC,
metadata: {
syncId: secretSync.id,
destination,
...req.body
}
}
});
return { secretSync };
}
});
server.route({
method: "PATCH",
url: "/:syncId",
config: {
rateLimit: writeLimit
},
schema: {
description: `Update the specified ${destinationName} Sync.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).syncId)
}),
body: updateSchema,
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.updateSecretSync(
{ ...req.body, syncId, destination },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretSync.projectId,
event: {
type: EventType.UPDATE_SECRET_SYNC,
metadata: {
syncId,
destination,
...req.body
}
}
});
return { secretSync };
}
});
server.route({
method: "DELETE",
url: `/:syncId`,
config: {
rateLimit: writeLimit
},
schema: {
description: `Delete the specified ${destinationName} Sync.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.DELETE(destination).syncId)
}),
querystring: z.object({
removeSecrets: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(SecretSyncs.DELETE(destination).removeSecrets)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const { removeSecrets } = req.query;
const secretSync = (await server.services.secretSync.deleteSecretSync(
{ destination, syncId, removeSecrets },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_SECRET_SYNC,
metadata: {
destination,
syncId,
removeSecrets
}
}
});
return { secretSync };
}
});
server.route({
method: "POST",
url: "/:syncId/sync-secrets",
config: {
rateLimit: writeLimit
},
schema: {
description: `Trigger a sync for the specified ${destinationName} Sync.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.SYNC_SECRETS(destination).syncId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.triggerSecretSyncSyncSecretsById(
{
syncId,
destination,
auditLogInfo: req.auditLogInfo
},
req.permission
)) as T;
return { secretSync };
}
});
server.route({
method: "POST",
url: "/:syncId/import-secrets",
config: {
rateLimit: writeLimit
},
schema: {
description: `Import secrets from the specified ${destinationName} Sync destination.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.IMPORT_SECRETS(destination).syncId)
}),
querystring: z.object({
importBehavior: z
.nativeEnum(SecretSyncImportBehavior)
.describe(SecretSyncs.IMPORT_SECRETS(destination).importBehavior)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const { importBehavior } = req.query;
const secretSync = (await server.services.secretSync.triggerSecretSyncImportSecretsById(
{
syncId,
destination,
importBehavior
},
req.permission
)) as T;
return { secretSync };
}
});
server.route({
method: "POST",
url: "/:syncId/remove-secrets",
config: {
rateLimit: writeLimit
},
schema: {
description: `Remove previously synced secrets from the specified ${destinationName} Sync destination.`,
params: z.object({
syncId: z.string().uuid().describe(SecretSyncs.REMOVE_SECRETS(destination).syncId)
}),
response: {
200: z.object({ secretSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { syncId } = req.params;
const secretSync = (await server.services.secretSync.triggerSecretSyncRemoveSecretsById(
{
syncId,
destination
},
req.permission
)) as T;
return { secretSync };
}
});
};

View File

@@ -0,0 +1,82 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSyncs } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
AwsParameterStoreSyncListItemSchema,
AwsParameterStoreSyncSchema
} from "@app/services/secret-sync/aws-parameter-store";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
const SecretSyncSchema = z.discriminatedUnion("destination", [AwsParameterStoreSyncSchema, GitHubSyncSchema]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncListItemSchema,
GitHubSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/options",
config: {
rateLimit: readLimit
},
schema: {
description: "List the available Secret Sync Options.",
response: {
200: z.object({
secretSyncOptions: SecretSyncOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const secretSyncOptions = server.services.secretSync.listSecretSyncOptions();
return { secretSyncOptions };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List all the Secret Syncs for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST().projectId)
}),
response: {
200: z.object({ secretSyncs: SecretSyncSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const secretSyncs = await server.services.secretSync.listSecretSyncsByProjectId({ projectId }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_SYNCS,
metadata: {
syncIds: secretSyncs.map((sync) => sync.id),
count: secretSyncs.length
}
}
});
return { secretSyncs };
}
});
};

View File

@@ -331,12 +331,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
failureAsync: async () => { failureAsync: async () => {
return res.redirect(appCfg.SITE_URL as string); return res.redirect(appCfg.SITE_URL as string);
}, },
successAsync: async (installation) => { successAsync: async () => {
const metadata = JSON.parse(installation.metadata || "") as { return res.redirect(`${appCfg.SITE_URL}/organization/settings?selectedTab=workflow-integrations`);
orgId: string;
};
return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings?selectedTab=workflow-integrations`);
} }
}); });
} }

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