Compare commits

...

131 Commits

Author SHA1 Message Date
Maidul Islam
eba12912f8 Merge pull request #3396 from akhilmhdh/feat/msg-crct
Updated error message on update org for saml/oidc enforcement
2025-04-20 12:07:07 -04:00
Maidul Islam
80edccc953 Update org-service.ts 2025-04-20 12:06:34 -04:00
Maidul Islam
f1b1d6f480 Merge pull request #3449 from Infisical/rbac-developer-role-correction
Documentation: Correct Developer Role Description
2025-04-18 14:19:09 -04:00
Akhil Mohan
07d6616f3c Merge pull request #3448 from akhilmhdh/feat/better-saml-error-message
Improved saml error messages
2025-04-18 22:36:59 +05:30
Scott Wilson
28d056cf7a documentation: correct developer description 2025-04-18 09:54:58 -07:00
=
f5d7809515 feat: improved saml error messages 2025-04-18 22:24:01 +05:30
Scott Wilson
233740e029 Merge pull request #3429 from Infisical/windmill-connection-and-sync
Feature: Windmill Connection and Sync
2025-04-17 17:53:19 -07:00
Daniel Hougaard
767fdc645f Merge branch 'heads/main' into windmill-connection-and-sync 2025-04-18 04:44:32 +04:00
Daniel Hougaard
c477703dda Merge pull request #3406 from juhnny5/patch-1
Typo correction in the go-sdk example
2025-04-18 03:46:45 +04:00
Scott Wilson
923d639c40 Merge pull request #3445 from Infisical/vercel-connection-validation-fix
Fix: Vercel Connection Validation API Call
2025-04-17 14:20:23 -07:00
Scott Wilson
7655dc7f3c chore: remove unused type 2025-04-17 12:22:02 -07:00
Scott Wilson
6c6c4db92c fix: use a non-team based api call for validation 2025-04-17 12:20:02 -07:00
Maidul Islam
8cf125ed32 Merge pull request #3444 from Infisical/default-project-delete-protection-false
Improvement: Set Project Delete Protection to False by Default
2025-04-17 12:21:46 -04:00
Scott Wilson
886cc9a113 improvement: set project delete protection to false by default 2025-04-17 09:16:36 -07:00
Daniel Hougaard
e1016f0a8b Merge pull request #3441 from Infisical/revert-3439-daniel/remove-docs
Revert "fix: removed legacy sdk's"
2025-04-17 07:05:30 +04:00
Daniel Hougaard
9c0a5f0bd4 fix: deprecation notices 2025-04-17 07:03:20 +04:00
Daniel Hougaard
7facd0e89e Revert "fix: removed legacy sdk's" 2025-04-17 05:52:07 +04:00
Scott Wilson
3afe2552d5 documentation: fix grammar 2025-04-16 15:59:18 -07:00
Scott Wilson
1fdb695240 deconflict merge 2025-04-16 15:40:18 -07:00
Scott Wilson
d9bd1ac878 improvements: address feedback 2025-04-16 15:28:29 -07:00
Maidul Islam
ee185cbe47 Merge pull request #3425 from akhilmhdh/feat/aws-cf-invalidate
Added aws cf invalidation on cli deployment pipeline
2025-04-16 17:22:10 -04:00
Maidul Islam
abc2f3808e Merge pull request #3438 from akhilmhdh/doc/sql-change
Updated doc on db permission change
2025-04-16 17:15:57 -04:00
Maidul Islam
733440a7b5 update docs for pg permissions 2025-04-16 17:15:11 -04:00
Maidul Islam
1ef3525917 Merge pull request #3439 from Infisical/daniel/remove-docs
fix: removed legacy sdk's
2025-04-16 16:42:58 -04:00
Daniel Hougaard
6664add428 fix: removed legacy sdk's 2025-04-17 00:41:29 +04:00
Daniel Hougaard
242e8fd2c6 Merge pull request #3424 from Infisical/misc/allow-org-admins-to-bypass-sso-enforcement
misc: allow org admins to bypass sso enforcement
2025-04-17 00:22:18 +04:00
Sheen Capadngan
1137247e69 misc: addressed feedback 2025-04-17 04:00:02 +08:00
=
32b951f6e9 doc: updated doc on db permission change 2025-04-17 01:09:23 +05:30
carlosmonastyrski
6f5fe053cd Merge pull request #3422 from Infisical/feat/addProjectDeletionProtection
Add project delete protection
2025-04-16 16:30:46 -03:00
Scott Wilson
875ec6a24e fix: lowercase workspace for url 2025-04-16 12:08:22 -07:00
carlosmonastyrski
17233e6a6f Merge pull request #3437 from Infisical/feat/addDocsOnSamlModal
Add SAML doc links to Org Settings
2025-04-16 14:20:28 -03:00
carlosmonastyrski
0dd06c1d66 Merge pull request #3419 from Infisical/feat/notifyOnServiceTokenExpiration
Add notification on Service Token expiration
2025-04-16 14:20:09 -03:00
Sheen Capadngan
fc2e5d18b7 misc: displayed full admin login url 2025-04-17 00:52:24 +08:00
Sheen
ae1ee25687 Merge pull request #3436 from Infisical/misc/made-jwt-signature-alg-configurable-for-oidc
misc: made jwt signature alg configurable for oidc
2025-04-17 00:47:16 +08:00
Sheen Capadngan
5d0bbce12d misc: added admin login url to tooltip 2025-04-17 00:41:42 +08:00
Sheen Capadngan
8c87c40467 misc: only bypass when from admin login 2025-04-17 00:33:07 +08:00
Sheen Capadngan
a9dab557d9 misc: correct labels 2025-04-17 00:06:27 +08:00
Sheen Capadngan
76c3f1c152 misc: made bypass opt-in 2025-04-16 23:58:20 +08:00
carlosmonastyrski
965084cc0c notifyExpiredTokens fixes 2025-04-16 12:48:00 -03:00
Scott Wilson
4650ba9fdd Merge pull request #3397 from Infisical/auth0-connection-and-secret-rotation
Feature: Auth0 Connection and Client Secret Rotation
2025-04-16 08:19:50 -07:00
carlosmonastyrski
73dea6a0be Merge branch 'main' into feat/addProjectDeletionProtection 2025-04-16 10:00:23 -03:00
carlosmonastyrski
e7742afcd3 Merge pull request #3434 from Infisical/fix/improveRandomValueGeneratorUI
Improve random value generator modal UI
2025-04-16 09:58:07 -03:00
carlosmonastyrski
7d3dd765ad Add SAML doc links to Org Settings 2025-04-16 09:52:58 -03:00
Sheen
927eb0407d misc: update documentation 2025-04-16 12:33:22 +00:00
Sheen Capadngan
17ddb79def misc: made jwt signature alg configurable for oidc 2025-04-16 20:20:37 +08:00
carlosmonastyrski
5ef5a5a107 Improve random value generator modal UI 2025-04-16 08:04:41 -03:00
carlosmonastyrski
9ae0880f50 Improve random value generator modal UI 2025-04-16 07:07:37 -03:00
Akhil Mohan
3814c65f38 Merge pull request #3433 from akhilmhdh/fix/permission
feat: dashboard failing on failing check
2025-04-16 13:00:43 +05:30
Akhil Mohan
3fa98e2a8d Merge pull request #3428 from x032205/cli-secrets-folders-get-path
Fixed `v1/folders` API backward compatibility with `directory` parameter
2025-04-16 12:49:02 +05:30
=
c6b21491db feat: dashboard failing on failing check 2025-04-16 12:37:39 +05:30
Scott Wilson
357381b0d6 feature: windmill connection and sync 2025-04-15 19:10:13 -07:00
carlosmonastyrski
82af77c480 Add hasDeleteProtection to update endpoint 2025-04-15 22:37:53 -03:00
x
b2fae5c439 fixed backward compatibility with --directory flag on every v1/folders endpoint 2025-04-15 18:46:08 -04:00
x
f16e96759f fixed --path not working with infisical secrets folders get 2025-04-15 18:42:01 -04:00
Scott Wilson
5eb9a1a667 improvement: add doc additions for single credential rotations 2025-04-15 15:07:39 -07:00
Scott Wilson
03ad6f822a merge deconflict 2025-04-15 14:32:21 -07:00
carlosmonastyrski
23a5a7a624 Improvements on notify expired service tokens 2025-04-15 18:31:05 -03:00
Scott Wilson
98447e9402 improvements: address feedback 2025-04-15 14:22:41 -07:00
Sheen
0f7e8585dc Merge pull request #3391 from Infisical/feat/add-metadata-based-permissions-for-dynamic-secret
feat: add metadata based permissions for dynamic secret
2025-04-16 04:28:52 +08:00
carlosmonastyrski
8568d1f6fe Merge pull request #3426 from Infisical/fix/addConfusedDeputyProblemOnAWSDocs
Add confused deputy problem to AWS assume role docs
2025-04-15 17:03:04 -03:00
carlosmonastyrski
27198869d8 Confused Deputy Attacks docs improvement 2025-04-15 16:55:05 -03:00
Sheen
dd0880825b doc: added reference to admin login portal 2025-04-15 19:50:49 +00:00
Maidul Islam
f27050a1c3 Merge pull request #3421 from x032205/san-size-limit
Increase Certificate Alternative Names (SAN) Character Limit to 4096
2025-04-15 15:29:58 -04:00
Sheen Capadngan
785173747f misc: introduce admin login portal 2025-04-16 03:28:04 +08:00
carlosmonastyrski
d33b06dd8a Add confused deputy problem to AWS assume role docs 2025-04-15 15:41:13 -03:00
=
9a6e27d4be feat: added aws cf invalidation on cli deployment pipeline 2025-04-15 23:41:06 +05:30
Sheen Capadngan
d0db5c00e8 misc: allow org admins to bypass sso enforcement 2025-04-16 01:35:39 +08:00
Scott Wilson
9475c1671e Merge pull request #3418 from Infisical/allow-internal-ip-connection-env-var
Feature: Add General Env Var for Allowing Internal IP Connections
2025-04-15 08:26:00 -07:00
Sheen
0f710b1ccc misc: updated documentation 2025-04-15 15:01:10 +00:00
Sheen Capadngan
71c55d5a53 misc: addressed review comments 2025-04-15 22:42:38 +08:00
Akhil Mohan
32bca651df Merge pull request #3423 from Infisical/fix/getFolderIsImportedByThrow
Avoid throwing on getFolderIsImportedBy no folder found
2025-04-15 18:51:51 +05:30
carlosmonastyrski
82533f49ca Avoid throwing on getFolderIsImportedBy no folder found 2025-04-15 10:19:41 -03:00
carlosmonastyrski
1d8c513da1 Improve invalidateQueries for useToggleDeleteProjectProtection 2025-04-15 08:07:21 -03:00
carlosmonastyrski
ae8a78b883 Fix cron schedule used to test 2025-04-15 07:46:35 -03:00
x
b08b53b77d increase certificate altnames character limit to 4096 2025-04-15 00:40:28 -04:00
Daniel Hougaard
862ed4f4e7 Merge pull request #3411 from Infisical/daniel/kms-signing-docs
docs(kms): KMS sign/verify docs
2025-04-15 05:39:21 +04:00
Daniel Hougaard
7b9254d09a Merge pull request #3358 from Infisical/daniel/go-sdk-kms-docs
docs(sdk): go sdk kms docs
2025-04-15 05:30:48 +04:00
Daniel Hougaard
c6305045e3 Revert "fix(docs): rename isDigest to preDigested"
This reverts commit 2642f7501d.
2025-04-15 05:28:41 +04:00
Daniel Hougaard
24bf9f7a2a Revert "fix: rename IsDigest to IsPreDigested"
This reverts commit 8d4fa0bdb9.
2025-04-15 05:24:39 +04:00
carlosmonastyrski
86d7fca8fb Add minor improvements to notifyExpiredTokens 2025-04-14 21:52:16 -03:00
carlosmonastyrski
cac4f30ca8 Update backend/src/services/service-token/service-token-dal.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-14 21:43:19 -03:00
carlosmonastyrski
101c056f43 Add project delete protection 2025-04-14 21:41:46 -03:00
Daniel Hougaard
8d4fa0bdb9 fix: rename IsDigest to IsPreDigested 2025-04-15 03:51:30 +04:00
Daniel Hougaard
2642f7501d fix(docs): rename isDigest to preDigested 2025-04-15 03:49:29 +04:00
Scott Wilson
68ba807b43 Merge pull request #3417 from Infisical/rollback-rotation-v1-deprecation
Improvement: Rollback Secret V1 Create Deprecation
2025-04-14 15:28:23 -07:00
carlosmonastyrski
80352acc8a Add notification on Service Token expiration 2025-04-14 18:31:06 -03:00
Scott Wilson
499ff3635b feature: add general env var for allowing internal ip connections and update relevant docs 2025-04-14 14:04:26 -07:00
carlosmonastyrski
78fc8a693d Merge pull request #3356 from Infisical/feat/showWarningOnImportedSecretDeletion
Add warning on secret deletions where it's being imported by another folder
2025-04-14 17:37:16 -03:00
Scott Wilson
78687984b7 Merge pull request #3404 from Infisical/native-integrations-ui-deprecation-for-sync-parity
Improvement: Native Integration Deprecation Details and Sync Redirect
2025-04-14 13:29:56 -07:00
Scott Wilson
25d3fb6a8c improvements: address feedback 2025-04-14 13:22:25 -07:00
carlosmonastyrski
31a4bcafbe Merge branch 'main' into feat/showWarningOnImportedSecretDeletion 2025-04-14 15:30:41 -03:00
carlosmonastyrski
ac8b3aca60 Merge pull request #3415 from Infisical/feat/addBackstagePluginsDocs
Add Backstage Plugins docs
2025-04-14 15:18:20 -03:00
carlosmonastyrski
4ea0cc62e3 Change External Integrations to Others 2025-04-14 15:07:16 -03:00
Sheen
bdab16f64b Merge pull request #3414 from Infisical/misc/add-proper-display-of-auth-failure-message
misc: add proper display of auth failure message for OIDC
2025-04-15 01:54:08 +08:00
Scott Wilson
9d0020fa4e improvement: rollback deprecate all secret rotation v1 create, update UI to only prevent pg/mssql 2025-04-14 10:50:45 -07:00
Akhil Mohan
3c07204532 Merge pull request #3416 from Infisical/daniel/make-idoment
fix: improve kms key migration
2025-04-14 23:08:59 +05:30
Daniel Hougaard
c0926bec69 fix: no check for encryption algorithm on external KMS 2025-04-14 21:36:38 +04:00
Daniel Hougaard
b9d74e0aed requested changes 2025-04-14 21:36:16 +04:00
Daniel Hougaard
f3078040fc fix: improve kms key migration 2025-04-14 21:22:59 +04:00
carlosmonastyrski
f2fead7a51 Add Backstage Plugins docs 2025-04-14 14:15:42 -03:00
carlosmonastyrski
3c58bf890d Merge branch 'main' into feat/showWarningOnImportedSecretDeletion 2025-04-14 09:04:47 -03:00
carlosmonastyrski
dc219b8e9f Fix edge case for referenced secrets batch delete and empty message 2025-04-14 08:54:43 -03:00
Daniel Hougaard
f1e30fd06b requested changes 2025-04-14 01:42:05 +04:00
Daniel Hougaard
e339b81bf1 docs(kms): signing documentation 2025-04-13 23:19:06 +04:00
Daniel Hougaard
b9bfe19b64 feat(kms/signing): better error handling 2025-04-13 23:17:50 +04:00
Julien Briault
fa030417ef Typo correction in the go-sdk example 2025-04-12 10:15:04 +02:00
Scott Wilson
8bfbae1037 chore: remove outdated comment 2025-04-11 19:38:47 -07:00
Scott Wilson
d00b34663e improvement: native integration legacy details and sync redirects 2025-04-11 19:34:33 -07:00
Scott Wilson
581e4b35f9 rebase 2025-04-11 12:25:26 -07:00
Sheen Capadngan
f33a777fae misc: updated form declaration for consistency 2025-04-12 02:04:49 +08:00
Sheen Capadngan
8a870131e9 misc: updated missing tx 2025-04-12 02:02:41 +08:00
Sheen Capadngan
d97057b43b misc: address metadata type 2025-04-12 01:50:05 +08:00
Sheen Capadngan
19b0cd9735 feat: update dynamic secret permissioning 2025-04-12 01:47:33 +08:00
=
7dcd3d24aa feat: corrected oidc message 2025-04-11 23:11:23 +05:30
=
3c5c6aeca8 feat: updated error message on update org for saml/oidc enforcement 2025-04-11 23:09:27 +05:30
carlosmonastyrski
1ec87fae75 Add referenced secret delete warning to overview page 2025-04-11 14:00:31 -03:00
carlosmonastyrski
aec131543f Add referenced secret delete warning to batch delete modal inside env 2025-04-11 10:12:41 -03:00
carlosmonastyrski
aeaa5babab Improve referenced secret deletion message logic 2025-04-11 09:29:01 -03:00
Sheen Capadngan
07898414a3 feat: add metadata based permissions for dynamic secret 2025-04-11 00:20:02 +08:00
carlosmonastyrski
f15b30ff85 Improve referenced secret deletion message component 2025-04-10 13:08:52 -03:00
carlosmonastyrski
8ee2b54182 Improve referenced secret deletion message component 2025-04-10 10:24:02 -03:00
carlosmonastyrski
b121ec891f UI changes on reference secret warning 2025-04-09 17:36:57 -03:00
carlosmonastyrski
ab566bcbe4 Merge branch 'main' into feat/showWarningOnImportedSecretDeletion 2025-04-09 15:39:43 -03:00
Daniel Hougaard
041d585f19 Update go.mdx 2025-04-09 02:11:43 +04:00
carlosmonastyrski
224b167000 Improve delete referenced secret warning message 2025-04-04 11:22:45 -03:00
Daniel Hougaard
e1a11c37e3 docs(sdk): go sdk kms docs 2025-04-04 06:02:47 +04:00
carlosmonastyrski
15130a433c UI improvements on secret deletion warning 2025-04-03 17:40:08 -03:00
carlosmonastyrski
a0bf03b2ae UI improvements on secret deletion warning 2025-04-03 16:00:41 -03:00
carlosmonastyrski
4d8598a019 Fix lint issue 2025-04-02 19:06:27 -03:00
carlosmonastyrski
a9da2d6241 Truncate folder name on warning message 2025-04-02 19:02:24 -03:00
carlosmonastyrski
4420985669 Add warning on secret deletions where it's being imported by another folder 2025-04-02 18:58:34 -03:00
368 changed files with 7693 additions and 1988 deletions

View File

@@ -145,3 +145,9 @@ jobs:
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }} INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
- name: Invalidate Cloudfront cache
run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }}

View File

@@ -22,3 +22,5 @@ frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredent
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28 frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65 frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
docs/documentation/platform/kms/overview.mdx:generic-api-key:344

View File

@@ -50,7 +50,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
- **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface. - **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface.
- **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more. - **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed. - **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed.
- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more. - **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more.
- **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more. - **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more.
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git. - **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git.
- **[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.

View File

@@ -136,7 +136,7 @@ declare module "fastify" {
rateLimits: RateLimitConfiguration; rateLimits: RateLimitConfiguration;
// passport data // passport data
passportUser: { passportUser: {
isUserCompleted: string; isUserCompleted: boolean;
providerAuthToken: string; providerAuthToken: string;
}; };
kmipUser: { kmipUser: {

View File

@@ -5,15 +5,21 @@ import { KmsKeyUsage } from "@app/services/kms/kms-types";
import { TableName } from "../schemas"; import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
const hasTypeColumn = await knex.schema.hasColumn(TableName.KmsKey, "type"); const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
await knex.schema.alterTable(TableName.KmsKey, (t) => { if (!hasKeyUsageColumn) {
if (!hasTypeColumn) t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT); await knex.schema.alterTable(TableName.KmsKey, (t) => {
}); t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
});
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.KmsKey, (t) => { const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
t.dropColumn("keyUsage");
}); if (hasKeyUsageColumn) {
await knex.schema.alterTable(TableName.KmsKey, (t) => {
t.dropColumn("keyUsage");
});
}
} }

View File

@@ -0,0 +1,20 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId"))) {
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
tb.uuid("dynamicSecretId");
tb.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId")) {
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
tb.dropColumn("dynamicSecretId");
});
}
}

View File

@@ -0,0 +1,27 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent");
if (!hasCol) {
await knex.schema.alterTable(TableName.ServiceToken, (t) => {
t.boolean("expiryNotificationSent").defaultTo(false);
});
// Update only tokens where expiresAt is before current time
await knex(TableName.ServiceToken)
.whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`)
.whereNotNull("expiresAt")
.update({ expiryNotificationSent: true });
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent");
if (hasCol) {
await knex.schema.alterTable(TableName.ServiceToken, (t) => {
t.dropColumn("expiryNotificationSent");
});
}
}

View File

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

View File

@@ -0,0 +1,15 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.string("altNames", 4096).alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.string("altNames").alter(); // Defaults to varchar(255)
});
}

View File

@@ -0,0 +1,15 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
t.string("altNames", 4096).alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
t.string("altNames").alter(); // Defaults to varchar(255)
});
}

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm"))) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.string("jwtSignatureAlgorithm").defaultTo(OIDCJWTSignatureAlgorithm.RS256).notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm")) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.dropColumn("jwtSignatureAlgorithm");
});
}
}

View File

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

View File

@@ -30,9 +30,10 @@ export const OidcConfigsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
lastUsed: z.date().nullable().optional(), lastUsed: z.date().nullable().optional(),
manageGroupMemberships: z.boolean().default(false),
encryptedOidcClientId: zodBuffer, encryptedOidcClientId: zodBuffer,
encryptedOidcClientSecret: zodBuffer encryptedOidcClientSecret: zodBuffer,
manageGroupMemberships: z.boolean().default(false),
jwtSignatureAlgorithm: z.string().default("RS256")
}); });
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>; export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;

View File

@@ -26,7 +26,8 @@ export const OrganizationsSchema = z.object({
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(), allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
shouldUseNewPrivilegeSystem: z.boolean().default(true), shouldUseNewPrivilegeSystem: z.boolean().default(true),
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional() privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
bypassOrgAuthEnabled: z.boolean().default(false)
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@@ -26,7 +26,8 @@ export const ProjectsSchema = z.object({
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) enforceCapitalization: z.boolean().default(false),
hasDeleteProtection: z.boolean().default(true).nullable().optional()
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -16,7 +16,8 @@ export const ResourceMetadataSchema = z.object({
identityId: z.string().uuid().nullable().optional(), identityId: z.string().uuid().nullable().optional(),
secretId: z.string().uuid().nullable().optional(), secretId: z.string().uuid().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
dynamicSecretId: z.string().uuid().nullable().optional()
}); });
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>; export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;

View File

@@ -21,7 +21,8 @@ export const ServiceTokensSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
createdBy: z.string(), createdBy: z.string(),
projectId: z.string() projectId: z.string(),
expiryNotificationSent: z.boolean().default(false).nullable().optional()
}); });
export type TServiceTokens = z.infer<typeof ServiceTokensSchema>; export type TServiceTokens = z.infer<typeof ServiceTokensSchema>;

View File

@@ -11,6 +11,7 @@ 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";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedDynamicSecretSchema } 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";
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => { export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -48,7 +49,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
.nullable(), .nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash), path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1), environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name) name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
metadata: ResourceMetadataSchema.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -143,7 +145,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}) })
.nullable(), .nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional() newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
metadata: ResourceMetadataSchema.optional()
}) })
}), }),
response: { response: {
@@ -238,6 +241,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
name: req.params.name, name: req.params.name,
...req.query ...req.query
}); });
return { dynamicSecret: dynamicSecretCfg }; return { dynamicSecret: dynamicSecretCfg };
} }
}); });

View File

@@ -12,7 +12,7 @@ import RedisStore from "connect-redis";
import { z } from "zod"; import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas"; import { OidcConfigsSchema } from "@app/db/schemas";
import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -30,7 +30,8 @@ const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({
orgId: true, orgId: true,
isActive: true, isActive: true,
allowedEmailDomains: true, allowedEmailDomains: true,
manageGroupMemberships: true manageGroupMemberships: true,
jwtSignatureAlgorithm: true
}); });
export const registerOidcRouter = async (server: FastifyZodProvider) => { export const registerOidcRouter = async (server: FastifyZodProvider) => {
@@ -170,7 +171,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
isActive: true, isActive: true,
orgId: true, orgId: true,
allowedEmailDomains: true, allowedEmailDomains: true,
manageGroupMemberships: true manageGroupMemberships: true,
jwtSignatureAlgorithm: true
}).extend({ }).extend({
clientId: z.string(), clientId: z.string(),
clientSecret: z.string() clientSecret: z.string()
@@ -225,7 +227,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
clientId: z.string().trim(), clientId: z.string().trim(),
clientSecret: z.string().trim(), clientSecret: z.string().trim(),
isActive: z.boolean(), isActive: z.boolean(),
manageGroupMemberships: z.boolean().optional() manageGroupMemberships: z.boolean().optional(),
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional()
}) })
.partial() .partial()
.merge(z.object({ orgSlug: z.string() })), .merge(z.object({ orgSlug: z.string() })),
@@ -292,7 +295,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
clientSecret: z.string().trim(), clientSecret: z.string().trim(),
isActive: z.boolean(), isActive: z.boolean(),
orgSlug: z.string().trim(), orgSlug: z.string().trim(),
manageGroupMemberships: z.boolean().optional().default(false) manageGroupMemberships: z.boolean().optional().default(false),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.default(OIDCJWTSignatureAlgorithm.RS256)
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) { if (data.configurationType === OIDCConfigurationType.CUSTOM) {

View File

@@ -223,12 +223,18 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
samlConfigId: z.string().trim() samlConfigId: z.string().trim()
}) })
}, },
preValidation: passport.authenticate("saml", { preValidation: passport.authenticate(
session: false, "saml",
failureFlash: true, {
failureRedirect: "/login/provider/error" session: false
// this is due to zod type difference },
}) as any, async (req, res, err, user) => {
if (err) {
throw new BadRequestError({ message: `Saml authentication failed. ${err?.message}`, error: err });
}
req.passportUser = user as { isUserCompleted: boolean; providerAuthToken: string };
}
) as any, // this is due to zod type difference
handler: (req, res) => { handler: (req, res) => {
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(

View File

@@ -23,7 +23,8 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
title: z.string(), title: z.string(),
image: z.string().optional(), image: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
template: z.any() template: z.any(),
isDeprecated: z.boolean().optional()
}) })
.array() .array()
}) })

View File

@@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas"; import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
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";
@@ -41,10 +40,16 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => { handler: async (req) => {
throw new BadRequestError({ const secretRotation = await server.services.secretRotation.createRotation({
message: `This version of Secret Rotations has been deprecated. Please see docs for new version.` actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId
}); });
return { secretRotation };
} }
}); });

View File

@@ -0,0 +1,19 @@
import {
Auth0ClientSecretRotationGeneratedCredentialsSchema,
Auth0ClientSecretRotationSchema,
CreateAuth0ClientSecretRotationSchema,
UpdateAuth0ClientSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerAuth0ClientSecretRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.Auth0ClientSecret,
server,
responseSchema: Auth0ClientSecretRotationSchema,
createSchema: CreateAuth0ClientSecretRotationSchema,
updateSchema: UpdateAuth0ClientSecretRotationSchema,
generatedCredentialsSchema: Auth0ClientSecretRotationGeneratedCredentialsSchema
});

View File

@@ -1,5 +1,6 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -10,5 +11,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
(server: FastifyZodProvider) => Promise<void> (server: FastifyZodProvider) => Promise<void>
> = { > = {
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
}; };

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -11,7 +12,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema, PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema
]); ]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {

View File

@@ -78,10 +78,6 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) { if (!plan?.dynamicSecret) {
@@ -102,6 +98,15 @@ export const dynamicSecretLeaseServiceFactory = ({
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found` message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id); const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT) if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
@@ -159,10 +164,6 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -187,7 +188,25 @@ export const dynamicSecretLeaseServiceFactory = ({
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
} }
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = await dynamicSecretDAL.findOne({
id: dynamicSecretLease.dynamicSecretId,
folderId: folder.id
});
if (!dynamicSecretCfg)
throw new NotFoundError({
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
@@ -239,10 +258,6 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -259,7 +274,25 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id) if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = await dynamicSecretDAL.findOne({
id: dynamicSecretLease.dynamicSecretId,
folderId: folder.id
});
if (!dynamicSecretCfg)
throw new NotFoundError({
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
@@ -309,10 +342,6 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) if (!folder)
@@ -326,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found` message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id }); const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
return dynamicSecretLeases; return dynamicSecretLeases;
}; };
@@ -352,10 +390,6 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` }); if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
@@ -364,6 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!dynamicSecretLease) if (!dynamicSecretLease)
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
id: dynamicSecretLease.dynamicSecretId,
folderId: folder.id
});
if (!dynamicSecretCfg)
throw new NotFoundError({
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
return dynamicSecretLease; return dynamicSecretLease;
}; };

View File

@@ -1,9 +1,17 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName, TDynamicSecrets } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import {
buildFindFilter,
ormify,
prependTableNameToFindFilter,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -12,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory
export const dynamicSecretDALFactory = (db: TDbClient) => { export const dynamicSecretDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecret); const orm = ormify(db, TableName.DynamicSecret);
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin(
TableName.ResourceMetadata,
`${TableName.ResourceMetadata}.dynamicSecretId`,
`${TableName.DynamicSecret}.id`
)
.select(selectAllTableCols(TableName.DynamicSecret))
.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")
)
.where(prependTableNameToFindFilter(TableName.DynamicSecret, filter));
const docs = sqlNestRelationships({
data: await query,
key: "id",
parentMapper: (el) => el,
childrenMapper: [
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return docs[0];
};
const findWithMetadata = async (
filter: TFindFilter<TDynamicSecrets>,
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {}
) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin(
TableName.ResourceMetadata,
`${TableName.ResourceMetadata}.dynamicSecretId`,
`${TableName.DynamicSecret}.id`
)
.select(selectAllTableCols(TableName.DynamicSecret))
.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")
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = sqlNestRelationships({
data: await query,
key: "id",
parentMapper: (el) => el,
childrenMapper: [
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return docs;
};
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination) // find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
const listDynamicSecretsByFolderIds = async ( const listDynamicSecretsByFolderIds = async (
{ {
@@ -39,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`); void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
} }
}) })
.leftJoin(
TableName.ResourceMetadata,
`${TableName.ResourceMetadata}.dynamicSecretId`,
`${TableName.DynamicSecret}.id`
)
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select( .select(
selectAllTableCols(TableName.DynamicSecret), selectAllTableCols(TableName.DynamicSecret),
db.ref("slug").withSchema(TableName.Environment).as("environment"), db.ref("slug").withSchema(TableName.Environment).as("environment"),
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`) db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`),
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
) )
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection); .orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
let queryWithLimit;
if (limit) { if (limit) {
const rankOffset = offset + 1; const rankOffset = offset + 1;
return await (tx || db) queryWithLimit = (tx || db.replicaNode())
.with("w", query) .with("w", query)
.select("*") .select("*")
.from<Awaited<typeof query>[number]>("w") .from<Awaited<typeof query>[number]>("w")
@@ -58,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
.andWhere("w.rank", "<", rankOffset + limit); .andWhere("w.rank", "<", rankOffset + limit);
} }
const dynamicSecrets = await query; const dynamicSecrets = sqlNestRelationships({
data: await (queryWithLimit || query),
key: "id",
parentMapper: (el) => el,
childrenMapper: [
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return dynamicSecrets; return dynamicSecrets;
} catch (error) { } catch (error) {
@@ -66,5 +178,5 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
} }
}; };
return { ...orm, listDynamicSecretsByFolderIds }; return { ...orm, listDynamicSecretsByFolderIds, findOne, findWithMetadata };
}; };

View File

@@ -42,7 +42,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
inputHostIps.push(...resolvedIps); inputHostIps.push(...resolvedIps);
} }
if (!isGateway && !appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP) { if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el)); const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" }); if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
} }

View File

@@ -12,6 +12,7 @@ import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
@@ -46,6 +47,7 @@ type TDynamicSecretServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">; projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>; export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -60,7 +62,8 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
kmsService, kmsService,
projectGatewayDAL projectGatewayDAL,
resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
path, path,
@@ -73,7 +76,8 @@ export const dynamicSecretServiceFactory = ({
projectSlug, projectSlug,
actorOrgId, actorOrgId,
defaultTTL, defaultTTL,
actorAuthMethod actorAuthMethod,
metadata
}: TCreateDynamicSecretDTO) => { }: TCreateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -87,9 +91,10 @@ export const dynamicSecretServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.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, metadata })
); );
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -131,16 +136,36 @@ export const dynamicSecretServiceFactory = ({
projectId projectId
}); });
const dynamicSecretCfg = await dynamicSecretDAL.create({ const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
type: provider.type, const cfg = await dynamicSecretDAL.create(
version: 1, {
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob, type: provider.type,
maxTTL, version: 1,
defaultTTL, encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
folderId: folder.id, maxTTL,
name, defaultTTL,
projectGatewayId: selectedGatewayId folderId: folder.id,
name,
projectGatewayId: selectedGatewayId
},
tx
);
if (metadata) {
await resourceMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
key,
value,
dynamicSecretId: cfg.id,
orgId: actorOrgId
})),
tx
);
}
return cfg;
}); });
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
@@ -156,7 +181,8 @@ export const dynamicSecretServiceFactory = ({
actorId, actorId,
newName, newName,
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod,
metadata
}: TUpdateDynamicSecretDTO) => { }: TUpdateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -171,10 +197,6 @@ export const dynamicSecretServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) { if (!plan?.dynamicSecret) {
@@ -193,6 +215,27 @@ export const dynamicSecretServiceFactory = ({
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
}); });
} }
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
if (metadata) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata
})
);
}
if (newName) { if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id }); const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
if (existingDynamicSecret) if (existingDynamicSecret)
@@ -231,14 +274,41 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob, const cfg = await dynamicSecretDAL.updateById(
maxTTL, dynamicSecretCfg.id,
defaultTTL, {
name: newName ?? name, encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
status: null, .cipherTextBlob,
statusDetails: null, maxTTL,
projectGatewayId: selectedGatewayId defaultTTL,
name: newName ?? name,
status: null,
projectGatewayId: selectedGatewayId
},
tx
);
if (metadata) {
await resourceMetadataDAL.delete(
{
dynamicSecretId: cfg.id
},
tx
);
await resourceMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
key,
value,
dynamicSecretId: cfg.id,
orgId: actorOrgId
})),
tx
);
}
return cfg;
}); });
return updatedDynamicCfg; return updatedDynamicCfg;
@@ -268,10 +338,6 @@ export const dynamicSecretServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) if (!folder)
@@ -282,6 +348,15 @@ export const dynamicSecretServiceFactory = ({
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` }); throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
} }
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id }); const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
// when not forced we check with the external system to first remove the things // when not forced we check with the external system to first remove the things
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system // we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
@@ -329,14 +404,6 @@ export const dynamicSecretServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) if (!folder)
@@ -346,6 +413,25 @@ export const dynamicSecretServiceFactory = ({
if (!dynamicSecretCfg) { if (!dynamicSecretCfg) {
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` }); throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
} }
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecretCfg.metadata
})
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId projectId
@@ -356,6 +442,7 @@ export const dynamicSecretServiceFactory = ({
) as object; ) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
return { ...dynamicSecretCfg, inputs: providerInputs }; return { ...dynamicSecretCfg, inputs: providerInputs };
}; };
@@ -426,7 +513,7 @@ export const dynamicSecretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ProjectPermissionSub.DynamicSecrets
); );
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
@@ -473,16 +560,12 @@ export const dynamicSecretServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) if (!folder)
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` }); throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
const dynamicSecretCfg = await dynamicSecretDAL.find( const dynamicSecretCfg = await dynamicSecretDAL.findWithMetadata(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined }, { folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
{ {
limit, limit,
@@ -490,7 +573,17 @@ export const dynamicSecretServiceFactory = ({
sort: orderBy ? [[orderBy, orderDirection]] : undefined sort: orderBy ? [[orderBy, orderDirection]] : undefined
} }
); );
return dynamicSecretCfg;
return dynamicSecretCfg.filter((dynamicSecret) => {
return permission.can(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: environmentSlug,
secretPath: path,
metadata: dynamicSecret.metadata
})
);
});
}; };
const listDynamicSecretsByFolderIds = async ( const listDynamicSecretsByFolderIds = async (
@@ -542,24 +635,14 @@ export const dynamicSecretServiceFactory = ({
isInternal, isInternal,
...params ...params
}: TListDynamicSecretsMultiEnvDTO) => { }: TListDynamicSecretsMultiEnvDTO) => {
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
actionProjectType: ActionProjectType.SecretManager });
});
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
)
);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
if (!folders.length) if (!folders.length)
@@ -572,7 +655,16 @@ export const dynamicSecretServiceFactory = ({
...params ...params
}); });
return dynamicSecretCfg; return dynamicSecretCfg.filter((dynamicSecret) => {
return permission.can(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, {
environment: dynamicSecret.environment,
secretPath: path,
metadata: dynamicSecret.metadata
})
);
});
}; };
const fetchAzureEntraIdUsers = async ({ const fetchAzureEntraIdUsers = async ({

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { DynamicSecretProviderSchema } from "./providers/models"; import { DynamicSecretProviderSchema } from "./providers/models";
@@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = {
environmentSlug: string; environmentSlug: string;
name: string; name: string;
projectSlug: string; projectSlug: string;
metadata?: ResourceMetadataDTO;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateDynamicSecretDTO = { export type TUpdateDynamicSecretDTO = {
@@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = {
environmentSlug: string; environmentSlug: string;
inputs?: TProvider["inputs"]; inputs?: TProvider["inputs"];
projectSlug: string; projectSlug: string;
metadata?: ResourceMetadataDTO;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteDynamicSecretDTO = { export type TDeleteDynamicSecretDTO = {

View File

@@ -1,6 +1,5 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>; export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
@@ -8,22 +7,5 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
export const oidcConfigDALFactory = (db: TDbClient) => { export const oidcConfigDALFactory = (db: TDbClient) => {
const oidcCfgOrm = ormify(db, TableName.OidcConfig); const oidcCfgOrm = ormify(db, TableName.OidcConfig);
const findEnforceableOidcCfg = async (orgId: string) => { return oidcCfgOrm;
try {
const oidcCfg = await db
.replicaNode()(TableName.OidcConfig)
.where({
orgId,
isActive: true
})
.whereNotNull("lastUsed")
.first();
return oidcCfg;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by id" });
}
};
return { ...oidcCfgOrm, findEnforceableOidcCfg };
}; };

View File

@@ -165,7 +165,8 @@ export const oidcConfigServiceFactory = ({
allowedEmailDomains: oidcCfg.allowedEmailDomains, allowedEmailDomains: oidcCfg.allowedEmailDomains,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships: oidcCfg.manageGroupMemberships manageGroupMemberships: oidcCfg.manageGroupMemberships,
jwtSignatureAlgorithm: oidcCfg.jwtSignatureAlgorithm
}; };
}; };
@@ -481,7 +482,8 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}: TUpdateOidcCfgDTO) => { }: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({
slug: orgSlug slug: orgSlug
@@ -536,7 +538,8 @@ export const oidcConfigServiceFactory = ({
jwksUri, jwksUri,
isActive, isActive,
lastUsed: null, lastUsed: null,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}; };
if (clientId !== undefined) { if (clientId !== undefined) {
@@ -569,7 +572,8 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}: TCreateOidcCfgDTO) => { }: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({
slug: orgSlug slug: orgSlug
@@ -613,6 +617,7 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
orgId: org.id, orgId: org.id,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm,
encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob,
encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob
}); });
@@ -676,7 +681,8 @@ export const oidcConfigServiceFactory = ({
const client = new issuer.Client({ const client = new issuer.Client({
client_id: oidcCfg.clientId, client_id: oidcCfg.clientId,
client_secret: oidcCfg.clientSecret, client_secret: oidcCfg.clientSecret,
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`] redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`],
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
}); });
const strategy = new OpenIdStrategy( const strategy = new OpenIdStrategy(

View File

@@ -5,6 +5,12 @@ export enum OIDCConfigurationType {
DISCOVERY_URL = "discoveryURL" DISCOVERY_URL = "discoveryURL"
} }
export enum OIDCJWTSignatureAlgorithm {
RS256 = "RS256",
HS256 = "HS256",
RS512 = "RS512"
}
export type TOidcLoginDTO = { export type TOidcLoginDTO = {
externalId: string; externalId: string;
email: string; email: string;
@@ -40,6 +46,7 @@ export type TCreateOidcCfgDTO = {
isActive: boolean; isActive: boolean;
orgSlug: string; orgSlug: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
} & TGenericPermission; } & TGenericPermission;
export type TUpdateOidcCfgDTO = Partial<{ export type TUpdateOidcCfgDTO = Partial<{
@@ -56,5 +63,6 @@ export type TUpdateOidcCfgDTO = Partial<{
isActive: boolean; isActive: boolean;
orgSlug: string; orgSlug: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
}> & }> &
TGenericPermission; TGenericPermission;

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { import {
IdentityProjectMembershipRoleSchema, IdentityProjectMembershipRoleSchema,
OrgMembershipRole,
OrgMembershipsSchema, OrgMembershipsSchema,
TableName, TableName,
TProjectRoles, TProjectRoles,
@@ -53,6 +54,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("permissions").withSchema(TableName.OrgRoles),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
db.ref("groupId").withSchema("userGroups"), db.ref("groupId").withSchema("userGroups"),
db.ref("groupOrgId").withSchema("userGroups"), db.ref("groupOrgId").withSchema("userGroups"),
db.ref("groupName").withSchema("userGroups"), db.ref("groupName").withSchema("userGroups"),
@@ -71,6 +73,7 @@ export const permissionDALFactory = (db: TDbClient) => {
OrgMembershipsSchema.extend({ OrgMembershipsSchema.extend({
permissions: z.unknown(), permissions: z.unknown(),
orgAuthEnforced: z.boolean().optional().nullable(), orgAuthEnforced: z.boolean().optional().nullable(),
bypassOrgAuthEnabled: z.boolean(),
customRoleSlug: z.string().optional().nullable(), customRoleSlug: z.string().optional().nullable(),
shouldUseNewPrivilegeSystem: z.boolean() shouldUseNewPrivilegeSystem: z.boolean()
}).parse(el), }).parse(el),
@@ -571,6 +574,11 @@ export const permissionDALFactory = (db: TDbClient) => {
}) })
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) .join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.join(TableName.OrgMembership, (qb) => {
void qb
.on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`);
})
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
@@ -670,6 +678,8 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId"), db.ref("id").withSchema(TableName.Project).as("projectId"),
@@ -683,6 +693,7 @@ export const permissionDALFactory = (db: TDbClient) => {
orgId, orgId,
username, username,
orgAuthEnforced, orgAuthEnforced,
orgRole,
membershipId, membershipId,
groupMembershipId, groupMembershipId,
membershipCreatedAt, membershipCreatedAt,
@@ -690,10 +701,12 @@ export const permissionDALFactory = (db: TDbClient) => {
groupMembershipUpdatedAt, groupMembershipUpdatedAt,
membershipUpdatedAt, membershipUpdatedAt,
projectType, projectType,
shouldUseNewPrivilegeSystem shouldUseNewPrivilegeSystem,
bypassOrgAuthEnabled
}) => ({ }) => ({
orgId, orgId,
orgAuthEnforced, orgAuthEnforced,
orgRole: orgRole as OrgMembershipRole,
userId, userId,
projectId, projectId,
username, username,
@@ -701,7 +714,8 @@ export const permissionDALFactory = (db: TDbClient) => {
id: membershipId || groupMembershipId, id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt, createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt, updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt,
shouldUseNewPrivilegeSystem shouldUseNewPrivilegeSystem,
bypassOrgAuthEnabled
}), }),
childrenMapper: [ childrenMapper: [
{ {

View File

@@ -2,7 +2,7 @@
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { TOrganizations } from "@app/db/schemas"; import { OrgMembershipRole, TOrganizations } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
@@ -118,11 +118,20 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
].includes(actorAuthMethod); ].includes(actorAuthMethod);
} }
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) { function validateOrgSSO(
actorAuthMethod: ActorAuthMethod,
isOrgSsoEnforced: TOrganizations["authEnforced"],
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
orgRole: OrgMembershipRole
) {
if (actorAuthMethod === undefined) { if (actorAuthMethod === undefined) {
throw new UnauthorizedError({ name: "No auth method defined" }); throw new UnauthorizedError({ name: "No auth method defined" });
} }
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
return;
}
if ( if (
isOrgSsoEnforced && isOrgSsoEnforced &&
actorAuthMethod !== null && actorAuthMethod !== null &&

View File

@@ -139,7 +139,12 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSSO(authMethod, membership.orgAuthEnforced); validateOrgSSO(
authMethod,
membership.orgAuthEnforced,
membership.bypassOrgAuthEnabled,
membership.role as OrgMembershipRole
);
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
membership?.groups?.map(({ role, customRolePermission }) => ({ membership?.groups?.map(({ role, customRolePermission }) => ({
@@ -226,7 +231,12 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); validateOrgSSO(
authMethod,
userProjectPermission.orgAuthEnforced,
userProjectPermission.bypassOrgAuthEnabled,
userProjectPermission.orgRole
);
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) {
throw new BadRequestError({ throw new BadRequestError({

View File

@@ -155,6 +155,10 @@ export type SecretFolderSubjectFields = {
export type DynamicSecretSubjectFields = { export type DynamicSecretSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
metadata?: {
key: string;
value: string;
}[];
}; };
export type SecretImportSubjectFields = { export type SecretImportSubjectFields = {
@@ -284,6 +288,42 @@ const SecretConditionV1Schema = z
}) })
.partial(); .partial();
const DynamicSecretConditionV2Schema = z
.object({
environment: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
]),
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
metadata: z.object({
[PermissionConditionOperators.$ELEMENTMATCH]: z
.object({
key: z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial(),
value: z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
})
.partial()
})
})
.partial();
const SecretConditionV2Schema = z const SecretConditionV2Schema = z
.object({ .object({
environment: z.union([ environment: z.union([
@@ -581,7 +621,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
), ),
conditions: SecretConditionV1Schema.describe( conditions: DynamicSecretConditionV2Schema.describe(
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),

View File

@@ -1,6 +1,5 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>; export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
@@ -8,25 +7,5 @@ export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
export const samlConfigDALFactory = (db: TDbClient) => { export const samlConfigDALFactory = (db: TDbClient) => {
const samlCfgOrm = ormify(db, TableName.SamlConfig); const samlCfgOrm = ormify(db, TableName.SamlConfig);
const findEnforceableSamlCfg = async (orgId: string) => { return samlCfgOrm;
try {
const samlCfg = await db
.replicaNode()(TableName.SamlConfig)
.where({
orgId,
isActive: true
})
.whereNotNull("lastUsed")
.first();
return samlCfg;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by id" });
}
};
return {
...samlCfgOrm,
findEnforceableSamlCfg
};
}; };

View File

@@ -0,0 +1,15 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "Auth0 Client Secret",
type: SecretRotation.Auth0ClientSecret,
connection: AppConnection.Auth0,
template: {
secretsMapping: {
clientId: "AUTH0_CLIENT_ID",
clientSecret: "AUTH0_CLIENT_SECRET"
}
}
};

View File

@@ -0,0 +1,104 @@
import {
TAuth0ClientSecretRotationGeneratedCredentials,
TAuth0ClientSecretRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { request } from "@app/lib/config/request";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0";
import { generatePassword } from "../shared/utils";
export const auth0ClientSecretRotationFactory: TRotationFactory<
TAuth0ClientSecretRotationWithConnection,
TAuth0ClientSecretRotationGeneratedCredentials
> = (secretRotation, appConnectionDAL, kmsService) => {
const {
connection,
parameters: { clientId },
secretsMapping
} = secretRotation;
const $rotateClientSecret = async () => {
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
const { audience } = connection.credentials;
await blockLocalAndPrivateIpAddresses(audience);
const clientSecret = generatePassword();
await request.request({
method: "PATCH",
url: `${audience}clients/${clientId}`,
headers: { authorization: `Bearer ${accessToken}` },
data: {
client_secret: clientSecret
}
});
return { clientId, clientSecret };
};
const issueCredentials: TRotationFactoryIssueCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
_,
callback
) => {
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
const { audience } = connection.credentials;
await blockLocalAndPrivateIpAddresses(audience);
// we just trigger an auth0 rotation to negate our credentials
await request.request({
method: "POST",
url: `${audience}clients/${clientId}/rotate-secret`,
headers: { authorization: `Bearer ${accessToken}` }
});
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAuth0ClientSecretRotationGeneratedCredentials> = (
generatedCredentials
) => {
const secrets = [
{
key: secretsMapping.clientId,
value: generatedCredentials.clientId
},
{
key: secretsMapping.clientSecret,
value: generatedCredentials.clientSecret
}
];
return secrets;
};
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -0,0 +1,67 @@
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { SecretRotations } from "@app/lib/api-docs";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const Auth0ClientSecretRotationGeneratedCredentialsSchema = z
.object({
clientId: z.string(),
clientSecret: z.string()
})
.array()
.min(1)
.max(2);
const Auth0ClientSecretRotationParametersSchema = z.object({
clientId: z
.string()
.trim()
.min(1, "Client ID Required")
.describe(SecretRotations.PARAMETERS.AUTH0_CLIENT_SECRET.clientId)
});
const Auth0ClientSecretRotationSecretsMappingSchema = z.object({
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientId),
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientSecret)
});
export const Auth0ClientSecretRotationTemplateSchema = z.object({
secretsMapping: z.object({
clientId: z.string(),
clientSecret: z.string()
})
});
export const Auth0ClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.Auth0ClientSecret).extend({
type: z.literal(SecretRotation.Auth0ClientSecret),
parameters: Auth0ClientSecretRotationParametersSchema,
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
});
export const CreateAuth0ClientSecretRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.Auth0ClientSecret
).extend({
parameters: Auth0ClientSecretRotationParametersSchema,
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
});
export const UpdateAuth0ClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.Auth0ClientSecret
).extend({
parameters: Auth0ClientSecretRotationParametersSchema.optional(),
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema.optional()
});
export const Auth0ClientSecretRotationListItemSchema = z.object({
name: z.literal("Auth0 Client Secret"),
connection: z.literal(AppConnection.Auth0),
type: z.literal(SecretRotation.Auth0ClientSecret),
template: Auth0ClientSecretRotationTemplateSchema
});

View File

@@ -0,0 +1,24 @@
import { z } from "zod";
import { TAuth0Connection } from "@app/services/app-connection/auth0";
import {
Auth0ClientSecretRotationGeneratedCredentialsSchema,
Auth0ClientSecretRotationListItemSchema,
Auth0ClientSecretRotationSchema,
CreateAuth0ClientSecretRotationSchema
} from "./auth0-client-secret-rotation-schemas";
export type TAuth0ClientSecretRotation = z.infer<typeof Auth0ClientSecretRotationSchema>;
export type TAuth0ClientSecretRotationInput = z.infer<typeof CreateAuth0ClientSecretRotationSchema>;
export type TAuth0ClientSecretRotationListItem = z.infer<typeof Auth0ClientSecretRotationListItemSchema>;
export type TAuth0ClientSecretRotationWithConnection = TAuth0ClientSecretRotation & {
connection: TAuth0Connection;
};
export type TAuth0ClientSecretRotationGeneratedCredentials = z.infer<
typeof Auth0ClientSecretRotationGeneratedCredentialsSchema
>;

View File

@@ -0,0 +1,3 @@
export * from "./auth0-client-secret-rotation-constants";
export * from "./auth0-client-secret-rotation-schemas";
export * from "./auth0-client-secret-rotation-types";

View File

@@ -1,6 +1,7 @@
export enum SecretRotation { export enum SecretRotation {
PostgresCredentials = "postgres-credentials", PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials" MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret"
} }
export enum SecretRotationStatus { export enum SecretRotationStatus {

View File

@@ -3,6 +3,7 @@ import { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -16,7 +17,8 @@ import {
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = { const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
}; };
export const listSecretRotationOptions = () => { export const listSecretRotationOptions = () => {

View File

@@ -3,10 +3,12 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = { export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials" [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
}; };
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = { export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql [SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
}; };

View File

@@ -13,6 +13,7 @@ import {
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { import {
calculateNextRotationAt, calculateNextRotationAt,
@@ -41,6 +42,7 @@ import {
TRotationFactory, TRotationFactory,
TSecretRotationRotateGeneratedCredentials, TSecretRotationRotateGeneratedCredentials,
TSecretRotationV2, TSecretRotationV2,
TSecretRotationV2GeneratedCredentials,
TSecretRotationV2Raw, TSecretRotationV2Raw,
TSecretRotationV2WithConnection, TSecretRotationV2WithConnection,
TUpdateSecretRotationV2DTO TUpdateSecretRotationV2DTO
@@ -53,6 +55,7 @@ import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { QueueJobs, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, TQueueServiceFactory } from "@app/queue";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns"; import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
@@ -97,15 +100,21 @@ export type TSecretRotationV2ServiceFactoryDep = {
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
queueService: Pick<TQueueServiceFactory, "queuePg">; queueService: Pick<TQueueServiceFactory, "queuePg">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
}; };
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>; export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
const MAX_GENERATED_CREDENTIALS_LENGTH = 2; const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactory> = { type TRotationFactoryImplementation = TRotationFactory<
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory, TSecretRotationV2WithConnection,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory TSecretRotationV2GeneratedCredentials
>;
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
}; };
export const secretRotationV2ServiceFactory = ({ export const secretRotationV2ServiceFactory = ({
@@ -125,7 +134,8 @@ export const secretRotationV2ServiceFactory = ({
secretQueueService, secretQueueService,
snapshotService, snapshotService,
keyStore, keyStore,
queueService queueService,
appConnectionDAL
}: TSecretRotationV2ServiceFactoryDep) => { }: TSecretRotationV2ServiceFactoryDep) => {
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -429,11 +439,15 @@ export const secretRotationV2ServiceFactory = ({
// validates permission to connect and app is valid for rotation type // validates permission to connect and app is valid for rotation type
const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor); const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor);
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({ const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type](
parameters: payload.parameters, {
secretsMapping, parameters: payload.parameters,
connection secretsMapping,
} as TSecretRotationV2WithConnection); connection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
);
try { try {
const currentTime = new Date(); const currentTime = new Date();
@@ -441,7 +455,7 @@ export const secretRotationV2ServiceFactory = ({
// callback structure to support transactional rollback when possible // callback structure to support transactional rollback when possible
const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => { const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => {
const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({ const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({
generatedCredentials: [newCredentials], generatedCredentials: [newCredentials] as TSecretRotationV2GeneratedCredentials,
projectId, projectId,
kmsService kmsService
}); });
@@ -740,32 +754,37 @@ export const secretRotationV2ServiceFactory = ({
message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
}); });
const deleteTransaction = secretRotationV2DAL.transaction(async (tx) => { const deleteTransaction = async () =>
if (deleteSecrets) { secretRotationV2DAL.transaction(async (tx) => {
await fnSecretBulkDelete({ if (deleteSecrets) {
secretDAL: secretV2BridgeDAL, await fnSecretBulkDelete({
secretQueueService, secretDAL: secretV2BridgeDAL,
inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({ secretQueueService,
secretKey, inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({
type: SecretType.Shared secretKey,
})), type: SecretType.Shared
projectId, })),
folderId, projectId,
actorId: actor.id, // not actually used since rotated secrets are shared folderId,
tx actorId: actor.id, // not actually used since rotated secrets are shared
}); tx
} });
}
return secretRotationV2DAL.deleteById(rotationId, tx); return secretRotationV2DAL.deleteById(rotationId, tx);
}); });
if (revokeGeneratedCredentials) { if (revokeGeneratedCredentials) {
const appConnection = await decryptAppConnection(connection, kmsService); const appConnection = await decryptAppConnection(connection, kmsService);
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type]({ const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type](
...secretRotation, {
connection: appConnection ...secretRotation,
} as TSecretRotationV2WithConnection); connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
);
const generatedCredentials = await decryptSecretRotationCredentials({ const generatedCredentials = await decryptSecretRotationCredentials({
encryptedGeneratedCredentials, encryptedGeneratedCredentials,
@@ -773,9 +792,9 @@ export const secretRotationV2ServiceFactory = ({
kmsService kmsService
}); });
await rotationFactory.revokeCredentials(generatedCredentials, async () => deleteTransaction); await rotationFactory.revokeCredentials(generatedCredentials, deleteTransaction);
} else { } else {
await deleteTransaction; await deleteTransaction();
} }
if (deleteSecrets) { if (deleteSecrets) {
@@ -840,10 +859,14 @@ export const secretRotationV2ServiceFactory = ({
const inactiveCredentials = generatedCredentials[inactiveIndex]; const inactiveCredentials = generatedCredentials[inactiveIndex];
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]({ const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation](
...secretRotation, {
connection: appConnection ...secretRotation,
} as TSecretRotationV2WithConnection); connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
);
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => { const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => {
const updatedCredentials = [...generatedCredentials]; const updatedCredentials = [...generatedCredentials];
@@ -851,7 +874,7 @@ export const secretRotationV2ServiceFactory = ({
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
projectId, projectId,
generatedCredentials: updatedCredentials, generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
kmsService kmsService
}); });

View File

@@ -1,8 +1,17 @@
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import {
TAuth0ClientSecretRotation,
TAuth0ClientSecretRotationGeneratedCredentials,
TAuth0ClientSecretRotationInput,
TAuth0ClientSecretRotationListItem,
TAuth0ClientSecretRotationWithConnection
} from "./auth0-client-secret";
import { import {
TMsSqlCredentialsRotation, TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput, TMsSqlCredentialsRotationInput,
@@ -18,17 +27,26 @@ import {
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation } from "./secret-rotation-v2-enums"; import { SecretRotation } from "./secret-rotation-v2-enums";
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation; export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
export type TSecretRotationV2WithConnection = export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection | TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection; | TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials = TSqlCredentialsRotationGeneratedCredentials; export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput; export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput;
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem; export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>; export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
@@ -129,27 +147,34 @@ export type TSecretRotationSendNotificationJobPayload = {
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the // transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
// third party credential changes (when supported), preventing credentials getting out of sync // third party credential changes (when supported), preventing credentials getting out of sync
export type TRotationFactoryIssueCredentials = ( export type TRotationFactoryIssueCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw> callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryRevokeCredentials = ( export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
generatedCredentials: TSecretRotationV2GeneratedCredentials, generatedCredentials: T,
callback: () => Promise<TSecretRotationV2Raw> callback: () => Promise<TSecretRotationV2Raw>
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryRotateCredentials = ( export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
credentialsToRevoke: TSecretRotationV2GeneratedCredentials[number] | undefined, credentialsToRevoke: T[number] | undefined,
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw> callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryGetSecretsPayload = ( export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
generatedCredentials: TSecretRotationV2GeneratedCredentials[number] generatedCredentials: T[number]
) => { key: string; value: string }[]; ) => { key: string; value: string }[];
export type TRotationFactory = (secretRotation: TSecretRotationV2WithConnection) => { export type TRotationFactory<
issueCredentials: TRotationFactoryIssueCredentials; T extends TSecretRotationV2WithConnection,
revokeCredentials: TRotationFactoryRevokeCredentials; C extends TSecretRotationV2GeneratedCredentials
rotateCredentials: TRotationFactoryRotateCredentials; > = (
getSecretsPayload: TRotationFactoryGetSecretsPayload; secretRotation: T,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
issueCredentials: TRotationFactoryIssueCredentials<C>;
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
rotateCredentials: TRotationFactoryRotateCredentials<C>;
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;
}; };

View File

@@ -1,9 +1,11 @@
import { z } from "zod"; import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema, PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema
]); ]);

View File

@@ -1,6 +1,5 @@
import { randomInt } from "crypto";
import { import {
TRotationFactory,
TRotationFactoryGetSecretsPayload, TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials, TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials, TRotationFactoryRevokeCredentials,
@@ -8,94 +7,12 @@ import {
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql"; import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
import { generatePassword } from "../utils";
import { import {
TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection TSqlCredentialsRotationWithConnection
} from "./sql-credentials-rotation-types"; } from "./sql-credentials-rotation-types";
const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
const generatePassword = () => {
try {
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};
const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => { const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => {
const error = e as Error; const error = e as Error;
@@ -110,7 +27,10 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
return redactedMessage; return redactedMessage;
}; };
export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRotationWithConnection) => { export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials
> = (secretRotation) => {
const { const {
connection, connection,
parameters: { username1, username2 }, parameters: { username1, username2 },
@@ -118,7 +38,7 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
secretsMapping secretsMapping
} = secretRotation; } = secretRotation;
const validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
const client = await getSqlConnectionClient({ const client = await getSqlConnectionClient({
...connection, ...connection,
credentials: { credentials: {
@@ -136,7 +56,9 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
} }
}; };
const issueCredentials: TRotationFactoryIssueCredentials = async (callback) => { const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
callback
) => {
const client = await getSqlConnectionClient(connection); const client = await getSqlConnectionClient(connection);
// For SQL, since we get existing users, we change both their passwords // For SQL, since we get existing users, we change both their passwords
@@ -159,13 +81,16 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
} }
for await (const credentials of credentialsSet) { for await (const credentials of credentialsSet) {
await validateCredentials(credentials); await $validateCredentials(credentials);
} }
return callback(credentialsSet[0]); return callback(credentialsSet[0]);
}; };
const revokeCredentials: TRotationFactoryRevokeCredentials = async (credentialsToRevoke, callback) => { const revokeCredentials: TRotationFactoryRevokeCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
credentialsToRevoke,
callback
) => {
const client = await getSqlConnectionClient(connection); const client = await getSqlConnectionClient(connection);
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
@@ -186,7 +111,10 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
return callback(); return callback();
}; };
const rotateCredentials: TRotationFactoryRotateCredentials = async (_, callback) => { const rotateCredentials: TRotationFactoryRotateCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
_,
callback
) => {
const client = await getSqlConnectionClient(connection); const client = await getSqlConnectionClient(connection);
// generate new password for the next active user // generate new password for the next active user
@@ -200,12 +128,14 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
await client.destroy(); await client.destroy();
} }
await validateCredentials(credentials); await $validateCredentials(credentials);
return callback(credentials); return callback(credentials);
}; };
const getSecretsPayload: TRotationFactoryGetSecretsPayload = (generatedCredentials) => { const getSecretsPayload: TRotationFactoryGetSecretsPayload<TSqlCredentialsRotationGeneratedCredentials> = (
generatedCredentials
) => {
const { username, password } = secretsMapping; const { username, password } = secretsMapping;
const secrets = [ const secrets = [
@@ -226,7 +156,6 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
issueCredentials, issueCredentials,
revokeCredentials, revokeCredentials,
rotateCredentials, rotateCredentials,
getSecretsPayload, getSecretsPayload
validateCredentials
}; };
}; };

View File

@@ -0,0 +1,84 @@
import { randomInt } from "crypto";
const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
export const generatePassword = () => {
try {
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};

View File

@@ -127,6 +127,13 @@ export const secretRotationServiceFactory = ({
}); });
if (selectedSecrets.length !== Object.values(outputs).length) if (selectedSecrets.length !== Object.values(outputs).length)
throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` }); throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` });
const rotatedSecrets = selectedSecrets.filter(({ isRotatedSecret }) => isRotatedSecret);
if (rotatedSecrets.length)
throw new BadRequestError({
message: `Selected secrets are already used for rotation: ${rotatedSecrets
.map((secret) => secret.key)
.join(", ")}`
});
} else { } else {
const selectedSecrets = await secretDAL.find({ const selectedSecrets = await secretDAL.find({
folderId: folder.id, folderId: folder.id,

View File

@@ -18,7 +18,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
title: "PostgreSQL", title: "PostgreSQL",
image: "postgres.png", image: "postgres.png",
description: "Rotate PostgreSQL/CockroachDB user credentials", description: "Rotate PostgreSQL/CockroachDB user credentials",
template: POSTGRES_TEMPLATE template: POSTGRES_TEMPLATE,
isDeprecated: true
}, },
{ {
name: "mysql", name: "mysql",
@@ -32,7 +33,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
title: "Microsoft SQL Server", title: "Microsoft SQL Server",
image: "mssqlserver.png", image: "mssqlserver.png",
description: "Rotate Microsoft SQL server user credentials", description: "Rotate Microsoft SQL server user credentials",
template: MSSQL_TEMPLATE template: MSSQL_TEMPLATE,
isDeprecated: true
}, },
{ {
name: "aws-iam", name: "aws-iam",

View File

@@ -50,6 +50,7 @@ export type TSecretRotationProviderTemplate = {
image?: string; image?: string;
description?: string; description?: string;
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate; template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
isDeprecated?: boolean;
}; };
export type THttpProviderTemplate = { export type THttpProviderTemplate = {

View File

@@ -478,7 +478,8 @@ export const PROJECTS = {
name: "The new name of the project.", name: "The new name of the project.",
projectDescription: "An optional description label for the project.", projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project.",
slug: "An optional slug for the project. (must be unique within the organization)" slug: "An optional slug for the project. (must be unique within the organization)",
hasDeleteProtection: "Enable or disable delete protection for the project."
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
@@ -1782,6 +1783,12 @@ export const AppConnections = {
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.`
}), }),
CREDENTIALS: { CREDENTIALS: {
AUTH0_CONNECTION: {
domain: "The domain of the Auth0 instance to connect to.",
clientId: "Your Auth0 application's Client ID.",
clientSecret: "Your Auth0 application's Client Secret.",
audience: "The unique identifier of the target API you want to access."
},
SQL_CONNECTION: { SQL_CONNECTION: {
host: "The hostname of the database server.", host: "The hostname of the database server.",
port: "The port number of the database.", port: "The port number of the database.",
@@ -1801,6 +1808,10 @@ export const AppConnections = {
CAMUNDA: { CAMUNDA: {
clientId: "The client ID used to authenticate with Camunda.", clientId: "The client ID used to authenticate with Camunda.",
clientSecret: "The client secret used to authenticate with Camunda." clientSecret: "The client secret used to authenticate with Camunda."
},
WINDMILL: {
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
accessToken: "The access token to use to connect with Windmill."
} }
} }
}; };
@@ -1936,6 +1947,10 @@ export const SecretSyncs = {
env: "The ID of the Vercel environment to sync secrets to.", env: "The ID of the Vercel environment to sync secrets to.",
branch: "The branch to sync preview secrets to.", branch: "The branch to sync preview secrets to.",
teamId: "The ID of the Vercel team to sync secrets to." teamId: "The ID of the Vercel team to sync secrets to."
},
WINDMILL: {
workspace: "The Windmill workspace to sync secrets to.",
path: "The Windmill workspace path to sync secrets to."
} }
} }
}; };
@@ -1997,12 +2012,19 @@ export const SecretRotations = {
"The username of the first login to rotate passwords for. This user must already exists in your database.", "The username of the first login to rotate passwords for. This user must already exists in your database.",
username2: username2:
"The username of the second login to rotate passwords for. This user must already exists in your database." "The username of the second login to rotate passwords for. This user must already exists in your database."
},
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
} }
}, },
SECRETS_MAPPING: { SECRETS_MAPPING: {
SQL_CREDENTIALS: { SQL_CREDENTIALS: {
username: "The name of the secret that the active username will be mapped to.", username: "The name of the secret that the active username will be mapped to.",
password: "The name of the secret that the generated password will be mapped to." password: "The name of the secret that the generated password will be mapped to."
},
AUTH0_CLIENT_SECRET: {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
} }
} }
}; };

View File

@@ -24,5 +24,6 @@ export enum PermissionConditionOperators {
$IN = "$in", $IN = "$in",
$EQ = "$eq", $EQ = "$eq",
$NEQ = "$ne", $NEQ = "$ne",
$GLOB = "$glob" $GLOB = "$glob",
$ELEMENTMATCH = "$elemMatch"
} }

View File

@@ -197,6 +197,7 @@ const envSchema = z
/* ----------------------------------------------------------------------------- */ /* ----------------------------------------------------------------------------- */
/* App Connections ----------------------------------------------------------------------------- */ /* App Connections ----------------------------------------------------------------------------- */
ALLOW_INTERNAL_IP_CONNECTIONS: zodStrBool.default("false"),
// aws // aws
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),

View File

@@ -118,7 +118,12 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
} }
}; };
const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => { const $signRsaDigest = async (
digest: Buffer,
privateKey: Buffer,
hashAlgorithm: SupportedHashAlgorithm,
signingAlgorithm: SigningAlgorithm
) => {
const tempDir = await createTemporaryDirectory("kms-rsa-sign"); const tempDir = await createTemporaryDirectory("kms-rsa-sign");
const digestPath = path.join(tempDir, "digest.bin"); const digestPath = path.join(tempDir, "digest.bin");
const sigPath = path.join(tempDir, "signature.bin"); const sigPath = path.join(tempDir, "signature.bin");
@@ -164,12 +169,22 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
} }
return signature; return signature;
} catch (err) {
logger.error(err, "KMS: Failed to sign RSA digest");
throw new BadRequestError({
message: `Failed to sign RSA digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
});
} finally { } finally {
await cleanTemporaryDirectory(tempDir); await cleanTemporaryDirectory(tempDir);
} }
}; };
const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => { const $signEccDigest = async (
digest: Buffer,
privateKey: Buffer,
hashAlgorithm: SupportedHashAlgorithm,
signingAlgorithm: SigningAlgorithm
) => {
const tempDir = await createTemporaryDirectory("ecc-sign"); const tempDir = await createTemporaryDirectory("ecc-sign");
const digestPath = path.join(tempDir, "digest.bin"); const digestPath = path.join(tempDir, "digest.bin");
const keyPath = path.join(tempDir, "key.pem"); const keyPath = path.join(tempDir, "key.pem");
@@ -216,6 +231,11 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
} }
return signature; return signature;
} catch (err) {
logger.error(err, "KMS: Failed to sign ECC digest");
throw new BadRequestError({
message: `Failed to sign ECC digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
});
} finally { } finally {
await cleanTemporaryDirectory(tempDir); await cleanTemporaryDirectory(tempDir);
} }
@@ -329,7 +349,12 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
const signDigestFunctionsMap: Record< const signDigestFunctionsMap: Record<
AsymmetricKeyAlgorithm, AsymmetricKeyAlgorithm,
(data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<Buffer> (
data: Buffer,
privateKey: Buffer,
hashAlgorithm: SupportedHashAlgorithm,
signingAlgorithm: SigningAlgorithm
) => Promise<Buffer>
> = { > = {
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest, [AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest [AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
@@ -360,7 +385,7 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
}); });
} }
const signature = await signFunction(data, privateKey, hashAlgorithm); const signature = await signFunction(data, privateKey, hashAlgorithm, signingAlgorithm);
return signature; return signature;
} }

View File

@@ -2,10 +2,16 @@ import dns from "node:dns/promises";
import { isIPv4 } from "net"; import { isIPv4 } from "net";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "../errors"; import { BadRequestError } from "../errors";
import { isPrivateIp } from "../ip/ipRange"; import { isPrivateIp } from "../ip/ipRange";
export const blockLocalAndPrivateIpAddresses = async (url: string) => { export const blockLocalAndPrivateIpAddresses = async (url: string) => {
const appCfg = getConfig();
if (appCfg.isDevelopmentMode) return;
const validUrl = new URL(url); const validUrl = new URL(url);
const inputHostIps: string[] = []; const inputHostIps: string[] = [];
if (isIPv4(validUrl.host)) { if (isIPv4(validUrl.host)) {
@@ -18,7 +24,8 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
inputHostIps.push(...resolvedIps); inputHostIps.push(...resolvedIps);
} }
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el)); const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" }); if (isInternalIp && !appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
}; };
type FQDNOptions = { type FQDNOptions = {

View File

@@ -1255,7 +1255,8 @@ export const registerRoutes = async (
userDAL, userDAL,
permissionService, permissionService,
projectDAL, projectDAL,
accessTokenQueue accessTokenQueue,
smtpService
}); });
const identityService = identityServiceFactory({ const identityService = identityServiceFactory({
@@ -1391,7 +1392,8 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService, licenseService,
kmsService, kmsService,
projectGatewayDAL projectGatewayDAL,
resourceMetadataDAL
}); });
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
@@ -1415,7 +1417,8 @@ export const registerRoutes = async (
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL, secretVersionV2DAL: secretVersionV2BridgeDAL,
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL,
serviceTokenService
}); });
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
@@ -1548,7 +1551,8 @@ export const registerRoutes = async (
resourceMetadataDAL, resourceMetadataDAL,
snapshotService, snapshotService,
secretQueueService, secretQueueService,
queueService queueService,
appConnectionDAL
}); });
await secretRotationV2QueueServiceFactory({ await secretRotationV2QueueServiceFactory({

View File

@@ -11,6 +11,7 @@ import {
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission"; import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
@@ -232,7 +233,11 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true, inputIV: true,
inputTag: true, inputTag: true,
algorithm: true algorithm: true
}); }).merge(
z.object({
metadata: ResourceMetadataSchema.optional()
})
);
export const SanitizedAuditLogStreamSchema = z.object({ export const SanitizedAuditLogStreamSchema = z.object({
id: z.string(), id: z.string(),
@@ -255,7 +260,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
upgradeStatus: true, upgradeStatus: true,
pitVersionLimit: true, pitVersionLimit: true,
kmsCertificateKeyId: true, kmsCertificateKeyId: true,
auditLogsRetentionDays: true auditLogsRetentionDays: true,
hasDeleteProtection: true
}); });
export const SanitizedTagSchema = SecretTagsSchema.pick({ export const SanitizedTagSchema = SecretTagsSchema.pick({

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws"; import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import { import {
AzureAppConfigurationConnectionListItemSchema, AzureAppConfigurationConnectionListItemSchema,
@@ -36,6 +37,10 @@ import {
TerraformCloudConnectionListItemSchema TerraformCloudConnectionListItemSchema
} from "@app/services/app-connection/terraform-cloud"; } from "@app/services/app-connection/terraform-cloud";
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel"; import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
import {
SanitizedWindmillConnectionSchema,
WindmillConnectionListItemSchema
} from "@app/services/app-connection/windmill";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps // can't use discriminated due to multiple schemas for certain apps
@@ -51,7 +56,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedVercelConnectionSchema.options, ...SanitizedVercelConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options, ...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options, ...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options ...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -66,7 +73,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
VercelConnectionListItemSchema, VercelConnectionListItemSchema,
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema, MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,51 @@
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 {
CreateAuth0ConnectionSchema,
SanitizedAuth0ConnectionSchema,
UpdateAuth0ConnectionSchema
} from "@app/services/app-connection/auth0";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerAuth0ConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Auth0,
server,
sanitizedResponseSchema: SanitizedAuth0ConnectionSchema,
createSchema: CreateAuth0ConnectionSchema,
updateSchema: UpdateAuth0ConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/clients`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
clients: z.object({ name: z.string(), id: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const clients = await server.services.appConnection.auth0.listClients(connectionId, req.permission);
return { clients };
}
});
};

View File

@@ -1,3 +1,4 @@
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router";
@@ -12,6 +13,7 @@ import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router";
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
export * from "./app-connection-router"; export * from "./app-connection-router";
@@ -28,5 +30,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Vercel]: registerVercelConnectionRouter, [AppConnection.Vercel]: registerVercelConnectionRouter,
[AppConnection.Postgres]: registerPostgresConnectionRouter, [AppConnection.Postgres]: registerPostgresConnectionRouter,
[AppConnection.MsSql]: registerMsSqlConnectionRouter, [AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter [AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.Windmill]: registerWindmillConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter
}; };

View File

@@ -0,0 +1,53 @@
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 {
CreateWindmillConnectionSchema,
SanitizedWindmillConnectionSchema,
UpdateWindmillConnectionSchema
} from "@app/services/app-connection/windmill";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerWindmillConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Windmill,
server,
sanitizedResponseSchema: SanitizedWindmillConnectionSchema,
createSchema: CreateWindmillConnectionSchema,
updateSchema: UpdateWindmillConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/workspaces`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const workspaces = await server.services.appConnection.windmill.listWorkspaces(connectionId, req.permission);
return workspaces;
}
});
};

View File

@@ -1,13 +1,9 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { SecretFoldersSchema, SecretImportsSchema } 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 { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
ProjectPermissionDynamicSecretActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { DASHBOARD } from "@app/lib/api-docs"; import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -142,6 +138,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}) })
.array() .array()
.optional(), .optional(),
importedByEnvs: z
.object({
environment: z.string(),
importedBy: z
.object({
environment: z.object({
name: z.string(),
slug: z.string()
}),
folders: z
.object({
name: z.string(),
isImported: z.boolean(),
secrets: z
.object({
secretId: z.string(),
referencedSecretKey: z.string()
})
.array()
.optional()
})
.array()
})
.array()
.optional()
})
.array()
.optional(),
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(), totalSecretCount: z.number().optional(),
@@ -289,24 +313,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalCount: totalFolderCount ?? 0 totalCount: totalFolderCount ?? 0
}; };
const { permission } = await server.services.permission.getProjectPermission({ if (includeDynamicSecrets) {
actor: req.permission.type,
actorId: req.permission.id,
projectId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
actionProjectType: ActionProjectType.SecretManager
});
const allowedDynamicSecretEnvironments = // filter envs user has access to
environments.filter((environment) =>
permission.can(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
)
);
if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) {
// this is the unique count, ie duplicate secrets across envs only count as 1 // this is the unique count, ie duplicate secrets across envs only count as 1
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({ totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
actor: req.permission.type, actor: req.permission.type,
@@ -315,7 +322,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId, projectId,
search, search,
environmentSlugs: allowedDynamicSecretEnvironments, environmentSlugs: environments,
path: secretPath, path: secretPath,
isInternal: true isInternal: true
}); });
@@ -330,7 +337,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search, search,
orderBy, orderBy,
orderDirection, orderDirection,
environmentSlugs: allowedDynamicSecretEnvironments, environmentSlugs: environments,
path: secretPath, path: secretPath,
limit: remainingLimit, limit: remainingLimit,
offset: adjustedOffset, offset: adjustedOffset,
@@ -471,6 +478,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
const importedByEnvs = [];
for await (const environment of environments) {
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
path: secretPath,
environment,
projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secrets: secrets?.filter((s) => s.environment === environment)
});
if (importedBy) {
importedByEnvs.push({
environment,
importedBy
});
}
}
return { return {
folders, folders,
dynamicSecrets, dynamicSecrets,
@@ -482,6 +511,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalImportCount, totalImportCount,
totalSecretCount, totalSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
importedByEnvs,
totalCount: totalCount:
(totalFolderCount ?? 0) + (totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) + (totalDynamicSecretCount ?? 0) +
@@ -575,6 +605,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(), totalSecretCount: z.number().optional(),
importedBy: z
.object({
environment: z.object({
name: z.string(),
slug: z.string()
}),
folders: z
.object({
name: z.string(),
isImported: z.boolean(),
secrets: z
.object({
secretId: z.string(),
referencedSecretKey: z.string()
})
.array()
.optional()
})
.array()
})
.array()
.optional(),
totalSecretRotationCount: z.number().optional(), totalSecretRotationCount: z.number().optional(),
totalCount: z.number() totalCount: z.number()
}) })
@@ -835,6 +887,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
path: secretPath,
environment,
projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secrets
});
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
const secretCount = const secretCount =
(secrets?.length ?? 0) + (secrets?.length ?? 0) +
@@ -880,6 +943,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalDynamicSecretCount, totalDynamicSecretCount,
totalSecretCount, totalSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
importedBy,
totalCount: totalCount:
(totalImportCount ?? 0) + (totalImportCount ?? 0) +
(totalFolderCount ?? 0) + (totalFolderCount ?? 0) +

View File

@@ -31,6 +31,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
.object({ .object({
name: z.string(), name: z.string(),
slug: z.string(), slug: z.string(),
syncSlug: z.string().optional(),
clientSlug: z.string().optional(), clientSlug: z.string().optional(),
image: z.string(), image: z.string(),
isAvailable: z.boolean().optional(), isAvailable: z.boolean().optional(),

View File

@@ -31,7 +31,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
organizations: sanitizedOrganizationSchema organizations: sanitizedOrganizationSchema
.extend({ .extend({
orgAuthMethod: z.string() orgAuthMethod: z.string(),
userRole: z.string()
}) })
.array() .array()
}) })
@@ -259,7 +260,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(), defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
enforceMfa: z.boolean().optional(), enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(), selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
allowSecretSharingOutsideOrganization: z.boolean().optional() allowSecretSharingOutsideOrganization: z.boolean().optional(),
bypassOrgAuthEnabled: z.boolean().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -312,6 +312,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.optional() .optional()
.describe(PROJECTS.UPDATE.projectDescription), .describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization), autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection),
slug: z slug: z
.string() .string()
.trim() .trim()
@@ -340,6 +341,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization, autoCapitalization: req.body.autoCapitalization,
hasDeleteProtection: req.body.hasDeleteProtection,
slug: req.body.slug slug: req.body.slug
}, },
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -390,6 +392,43 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "POST",
url: "/:workspaceId/delete-protection",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
hasDeleteProtection: z.boolean()
}),
response: {
200: z.object({
message: z.string(),
workspace: SanitizedProjectSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.toggleDeleteProtection({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
hasDeleteProtection: req.body.hasDeleteProtection
});
return {
message: "Successfully changed workspace settings",
workspace
};
}
});
server.route({ server.route({
method: "PUT", method: "PUT",
url: "/:workspaceSlug/version-limit", url: "/:workspaceSlug/version-limit",

View File

@@ -39,17 +39,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.path), .describe(FOLDERS.CREATE.path)
.optional(),
// backward compatiability with cli // backward compatiability with cli
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.directory), .describe(FOLDERS.CREATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
}), }),
response: { response: {
@@ -60,7 +62,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory; const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.createFolder({ const folder = await server.services.folder.createFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -120,17 +122,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path), .describe(FOLDERS.UPDATE.path)
.optional(),
// backward compatiability with cli // backward compatiability with cli
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.directory), .describe(FOLDERS.UPDATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
}), }),
response: { response: {
@@ -141,7 +145,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory; const path = req.body.path || req.body.directory || "/";
const { folder, old } = await server.services.folder.updateFolder({ const { folder, old } = await server.services.folder.updateFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -271,17 +275,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.path), .describe(FOLDERS.DELETE.path)
.optional(),
// keep this here as cli need directory // keep this here as cli need directory
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.directory) .describe(FOLDERS.DELETE.directory)
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -291,7 +297,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory; const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.deleteFolder({ const folder = await server.services.folder.deleteFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -339,18 +345,18 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
path: z path: z
.string() .string()
.trim() .trim()
.default("/") .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.LIST.path), .describe(FOLDERS.LIST.path)
.optional(),
// backward compatiability with cli // backward compatiability with cli
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.LIST.directory), .describe(FOLDERS.LIST.directory)
.optional(),
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
}), }),
response: { response: {
@@ -363,7 +369,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.query.path || req.query.directory; const path = req.query.path || req.query.directory || "/";
const folders = await server.services.folder.getFolders({ const folders = await server.services.folder.getFolders({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,

View File

@@ -11,6 +11,7 @@ import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router";
import { registerWindmillSyncRouter } from "./windmill-sync-router";
export * from "./secret-sync-router"; export * from "./secret-sync-router";
@@ -25,5 +26,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Humanitec]: registerHumanitecSyncRouter, [SecretSync.Humanitec]: registerHumanitecSyncRouter,
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter, [SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
[SecretSync.Camunda]: registerCamundaSyncRouter, [SecretSync.Camunda]: registerCamundaSyncRouter,
[SecretSync.Vercel]: registerVercelSyncRouter [SecretSync.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter
}; };

View File

@@ -25,6 +25,7 @@ import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
const SecretSyncSchema = z.discriminatedUnion("destination", [ const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema, AwsParameterStoreSyncSchema,
@@ -37,7 +38,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
HumanitecSyncSchema, HumanitecSyncSchema,
TerraformCloudSyncSchema, TerraformCloudSyncSchema,
CamundaSyncSchema, CamundaSyncSchema,
VercelSyncSchema VercelSyncSchema,
WindmillSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -51,7 +53,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
HumanitecSyncListItemSchema, HumanitecSyncListItemSchema,
TerraformCloudSyncListItemSchema, TerraformCloudSyncListItemSchema,
CamundaSyncListItemSchema, CamundaSyncListItemSchema,
VercelSyncListItemSchema VercelSyncListItemSchema,
WindmillSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,17 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
CreateWindmillSyncSchema,
UpdateWindmillSyncSchema,
WindmillSyncSchema
} from "@app/services/secret-sync/windmill";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerWindmillSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Windmill,
server,
responseSchema: WindmillSyncSchema,
createSchema: CreateWindmillSyncSchema,
updateSchema: UpdateWindmillSyncSchema
});

View File

@@ -303,7 +303,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name), name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription), description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection)
}), }),
response: { response: {
200: SanitizedProjectSchema 200: SanitizedProjectSchema
@@ -321,7 +322,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
update: { update: {
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization autoCapitalization: req.body.autoCapitalization,
hasDeleteProtection: req.body.hasDeleteProtection
}, },
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,

View File

@@ -10,7 +10,9 @@ export enum AppConnection {
Vercel = "vercel", Vercel = "vercel",
Postgres = "postgres", Postgres = "postgres",
MsSql = "mssql", MsSql = "mssql",
Camunda = "camunda" Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@@ -16,6 +16,7 @@ import {
TAppConnectionCredentialsValidator, TAppConnectionCredentialsValidator,
TAppConnectionTransitionCredentialsToPlatform TAppConnectionTransitionCredentialsToPlatform
} from "./app-connection-types"; } from "./app-connection-types";
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws"; import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
import { import {
AzureAppConfigurationConnectionMethod, AzureAppConfigurationConnectionMethod,
@@ -49,6 +50,11 @@ import {
} from "./terraform-cloud"; } from "./terraform-cloud";
import { VercelConnectionMethod } from "./vercel"; import { VercelConnectionMethod } from "./vercel";
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns"; import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
import {
getWindmillConnectionListItem,
validateWindmillConnectionCredentials,
WindmillConnectionMethod
} from "./windmill";
export const listAppConnectionOptions = () => { export const listAppConnectionOptions = () => {
return [ return [
@@ -63,7 +69,9 @@ export const listAppConnectionOptions = () => {
getVercelConnectionListItem(), getVercelConnectionListItem(),
getPostgresConnectionListItem(), getPostgresConnectionListItem(),
getMsSqlConnectionListItem(), getMsSqlConnectionListItem(),
getCamundaConnectionListItem() getCamundaConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -109,25 +117,29 @@ export const decryptAppConnectionCredentials = async ({
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
}; };
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureAppConfiguration]:
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
};
export const validateAppConnectionCredentials = async ( export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig appConnection: TAppConnectionConfig
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); ): Promise<TAppConnection["credentials"]> => {
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureAppConfiguration]:
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
};
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
switch (method) { switch (method) {
@@ -154,6 +166,10 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
return "Username & Password"; return "Username & Password";
case WindmillConnectionMethod.AccessToken:
return "Access Token";
case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials";
default: default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`); throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -196,5 +212,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported, [AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
[AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported [AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
}; };

View File

@@ -12,5 +12,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Vercel]: "Vercel", [AppConnection.Vercel]: "Vercel",
[AppConnection.Postgres]: "PostgreSQL", [AppConnection.Postgres]: "PostgreSQL",
[AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.MsSql]: "Microsoft SQL Server",
[AppConnection.Camunda]: "Camunda" [AppConnection.Camunda]: "Camunda",
[AppConnection.Windmill]: "Windmill",
[AppConnection.Auth0]: "Auth0"
}; };

View File

@@ -14,6 +14,7 @@ import {
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
validateAppConnectionCredentials validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns"; } from "@app/services/app-connection/app-connection-fns";
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal"; import { TAppConnectionDALFactory } from "./app-connection-dal";
@@ -27,6 +28,7 @@ import {
TUpdateAppConnectionDTO, TUpdateAppConnectionDTO,
TValidateAppConnectionCredentialsSchema TValidateAppConnectionCredentialsSchema
} from "./app-connection-types"; } from "./app-connection-types";
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service"; import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
@@ -47,6 +49,8 @@ import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-c
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service"; import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
import { ValidateVercelConnectionCredentialsSchema } from "./vercel"; import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
import { vercelConnectionService } from "./vercel/vercel-connection-service"; import { vercelConnectionService } from "./vercel/vercel-connection-service";
import { ValidateWindmillConnectionCredentialsSchema } from "./windmill";
import { windmillConnectionService } from "./windmill/windmill-connection-service";
export type TAppConnectionServiceFactoryDep = { export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
@@ -68,7 +72,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema, [AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema, [AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema, [AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema [AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -442,6 +448,8 @@ export const appConnectionServiceFactory = ({
humanitec: humanitecConnectionService(connectAppConnectionById), humanitec: humanitecConnectionService(connectAppConnectionById),
terraformCloud: terraformCloudConnectionService(connectAppConnectionById), terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById) vercel: vercelConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
}; };
}; };

View File

@@ -3,6 +3,12 @@ import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sq
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { AWSRegion } from "./app-connection-enums"; import { AWSRegion } from "./app-connection-enums";
import {
TAuth0Connection,
TAuth0ConnectionConfig,
TAuth0ConnectionInput,
TValidateAuth0ConnectionCredentialsSchema
} from "./auth0";
import { import {
TAwsConnection, TAwsConnection,
TAwsConnectionConfig, TAwsConnectionConfig,
@@ -69,6 +75,12 @@ import {
TVercelConnectionConfig, TVercelConnectionConfig,
TVercelConnectionInput TVercelConnectionInput
} from "./vercel"; } from "./vercel";
import {
TValidateWindmillConnectionCredentialsSchema,
TWindmillConnection,
TWindmillConnectionConfig,
TWindmillConnectionInput
} from "./windmill";
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
@@ -83,6 +95,8 @@ export type TAppConnection = { id: string } & (
| TPostgresConnection | TPostgresConnection
| TMsSqlConnection | TMsSqlConnection
| TCamundaConnection | TCamundaConnection
| TWindmillConnection
| TAuth0Connection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -102,6 +116,8 @@ export type TAppConnectionInput = { id: string } & (
| TPostgresConnectionInput | TPostgresConnectionInput
| TMsSqlConnectionInput | TMsSqlConnectionInput
| TCamundaConnectionInput | TCamundaConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -126,7 +142,9 @@ export type TAppConnectionConfig =
| TTerraformCloudConnectionConfig | TTerraformCloudConnectionConfig
| TVercelConnectionConfig | TVercelConnectionConfig
| TSqlConnectionConfig | TSqlConnectionConfig
| TCamundaConnectionConfig; | TCamundaConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -140,7 +158,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateMsSqlConnectionCredentialsSchema | TValidateMsSqlConnectionCredentialsSchema
| TValidateCamundaConnectionCredentialsSchema | TValidateCamundaConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema | TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema; | TValidateVercelConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@@ -0,0 +1,3 @@
export enum Auth0ConnectionMethod {
ClientCredentials = "client-credentials"
}

View File

@@ -0,0 +1,97 @@
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
import { TAuth0AccessTokenResponse, TAuth0Connection, TAuth0ConnectionConfig } from "./auth0-connection-types";
export const getAuth0ConnectionListItem = () => {
return {
name: "Auth0" as const,
app: AppConnection.Auth0 as const,
methods: Object.values(Auth0ConnectionMethod) as [Auth0ConnectionMethod.ClientCredentials]
};
};
const authorizeAuth0Connection = async ({
clientId,
clientSecret,
domain,
audience
}: TAuth0ConnectionConfig["credentials"]) => {
const instanceUrl = domain.startsWith("http") ? domain : `https://${domain}`;
await blockLocalAndPrivateIpAddresses(instanceUrl);
const { data } = await request.request<TAuth0AccessTokenResponse>({
method: "POST",
url: `${removeTrailingSlash(instanceUrl)}/oauth/token`,
headers: { "content-type": "application/x-www-form-urlencoded" },
data: new URLSearchParams({
grant_type: "client_credentials", // this will need to be resolved if we support methods other than client credentials
client_id: clientId,
client_secret: clientSecret,
audience
})
});
if (data.token_type !== "Bearer") {
throw new Error(`Unhandled token type: ${data.token_type}`);
}
return {
accessToken: data.access_token,
// cap token lifespan to 10 minutes
expiresAt: Math.min(data.expires_in * 1000, 600000) + Date.now()
};
};
export const getAuth0ConnectionAccessToken = async (
{ id, orgId, credentials }: TAuth0Connection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const { expiresAt, accessToken } = credentials;
// get new token if expired or less than 5 minutes until expiry
if (Date.now() < expiresAt - 300000) {
return accessToken;
}
const authData = await authorizeAuth0Connection(credentials);
const updatedCredentials: TAuth0Connection["credentials"] = {
...credentials,
...authData
};
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
});
await appConnectionDAL.updateById(id, { encryptedCredentials });
return authData.accessToken;
};
export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0ConnectionConfig) => {
try {
const { accessToken, expiresAt } = await authorizeAuth0Connection(credentials);
return {
...credentials,
accessToken,
expiresAt
};
} catch (e: unknown) {
throw new BadRequestError({
message: (e as Error).message ?? `Unable to validate connection: verify credentials`
});
}
};

View File

@@ -0,0 +1,94 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({
domain: z.string().trim().min(1, "Domain required").describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.domain),
clientId: z
.string()
.trim()
.min(1, "Client ID required")
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientId),
clientSecret: z
.string()
.trim()
.min(1, "Client Secret required")
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientSecret),
audience: z
.string()
.trim()
.url()
.min(1, "Audience required")
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.audience)
});
const Auth0ConnectionClientCredentialsOutputCredentialsSchema = z
.object({
accessToken: z.string(),
expiresAt: z.number()
})
.merge(Auth0ConnectionClientCredentialsInputCredentialsSchema);
const BaseAuth0ConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Auth0)
});
export const Auth0ConnectionSchema = z.intersection(
BaseAuth0ConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
credentials: Auth0ConnectionClientCredentialsOutputCredentialsSchema
})
])
);
export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [
BaseAuth0ConnectionSchema.extend({
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.pick({
domain: true,
clientId: true,
audience: true
})
})
]);
export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(Auth0ConnectionMethod.ClientCredentials)
.describe(AppConnections.CREATE(AppConnection.Auth0).method),
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Auth0).credentials
)
})
]);
export const CreateAuth0ConnectionSchema = ValidateAuth0ConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Auth0)
);
export const UpdateAuth0ConnectionSchema = z
.object({
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Auth0).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0));
export const Auth0ConnectionListItemSchema = z.object({
name: z.literal("Auth0"),
app: z.literal(AppConnection.Auth0),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(Auth0ConnectionMethod).array()
});

View File

@@ -0,0 +1,71 @@
import { request } from "@app/lib/config/request";
import { OrgServiceActor } from "@app/lib/types";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0/auth0-connection-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAuth0Connection, TAuth0ListClient, TAuth0ListClientsResponse } from "./auth0-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TAuth0Connection>;
const listAuth0Clients = async (
appConnection: TAuth0Connection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const accessToken = await getAuth0ConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
const { audience, clientId: connectionClientId } = appConnection.credentials;
await blockLocalAndPrivateIpAddresses(audience);
const clients: TAuth0ListClient[] = [];
let hasMore = true;
let page = 0;
while (hasMore) {
// eslint-disable-next-line no-await-in-loop
const { data: clientsPage } = await request.get<TAuth0ListClientsResponse>(`${audience}clients`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
},
params: {
include_totals: true,
per_page: 100,
page
}
});
clients.push(...clientsPage.clients);
page += 1;
hasMore = clientsPage.total > clients.length;
}
return (
clients.filter((client) => client.client_id !== connectionClientId && client.name !== "All Applications") ?? []
);
};
export const auth0ConnectionService = (
getAppConnection: TGetAppConnectionFunc,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const listClients = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Auth0, connectionId, actor);
const clients = await listAuth0Clients(appConnection, appConnectionDAL, kmsService);
return clients.map((client) => ({ id: client.client_id, name: client.name }));
};
return {
listClients
};
};

View File

@@ -0,0 +1,39 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
Auth0ConnectionSchema,
CreateAuth0ConnectionSchema,
ValidateAuth0ConnectionCredentialsSchema
} from "./auth0-connection-schemas";
export type TAuth0Connection = z.infer<typeof Auth0ConnectionSchema>;
export type TAuth0ConnectionInput = z.infer<typeof CreateAuth0ConnectionSchema> & {
app: AppConnection.Auth0;
};
export type TValidateAuth0ConnectionCredentialsSchema = typeof ValidateAuth0ConnectionCredentialsSchema;
export type TAuth0ConnectionConfig = DiscriminativePick<TAuth0Connection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TAuth0AccessTokenResponse = {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
};
export type TAuth0ListClient = {
name: string;
client_id: string;
};
export type TAuth0ListClientsResponse = {
total: number;
clients: TAuth0ListClient[];
};

View File

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

View File

@@ -1,6 +1,7 @@
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
@@ -26,6 +27,8 @@ const authorizeDatabricksConnection = async ({
clientSecret, clientSecret,
workspaceUrl workspaceUrl
}: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => { }: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
await blockLocalAndPrivateIpAddresses(workspaceUrl);
const { data } = await request.post<TAuthorizeDatabricksConnection>( const { data } = await request.post<TAuthorizeDatabricksConnection>(
`${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`, `${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`,
"grant_type=client_credentials&scope=all-apis", "grant_type=client_credentials&scope=all-apis",

View File

@@ -16,7 +16,7 @@ export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.obje
workspaceUrl: z.string().trim().url().min(1, "Workspace URL required") workspaceUrl: z.string().trim().url().min(1, "Workspace URL required")
}); });
export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
.object({ .object({
accessToken: z.string(), accessToken: z.string(),
expiresAt: z.number() expiresAt: z.number()

View File

@@ -44,7 +44,7 @@ export const validateTerraformCloudConnectionCredentials = async (config: TTerra
}); });
} }
throw new BadRequestError({ throw new BadRequestError({
message: "Unable to validate connection - verify credentials" message: "Unable to validate connection: verify credentials"
}); });
} }

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { AxiosError, AxiosResponse } from "axios"; import { AxiosError } from "axios";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, InternalServerError } from "@app/lib/errors";
@@ -27,10 +27,8 @@ export const getVercelConnectionListItem = () => {
export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => { export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => {
const { credentials: inputCredentials } = config; const { credentials: inputCredentials } = config;
let response: AxiosResponse<VercelApp[]> | null = null;
try { try {
response = await request.get<VercelApp[]>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, { await request.get(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, {
headers: { headers: {
Authorization: `Bearer ${inputCredentials.apiToken}` Authorization: `Bearer ${inputCredentials.apiToken}`
} }
@@ -38,17 +36,14 @@ export const validateVercelConnectionCredentials = async (config: TVercelConnect
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}` // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to validate credentials: ${
error.response?.data ? JSON.stringify(error.response?.data) : error.message || "Unknown error"
}`
}); });
} }
throw new BadRequestError({ throw new BadRequestError({
message: "Unable to validate connection - verify credentials" message: `Unable to validate connection: ${(error as Error).message || "Verify credentials"}`
});
}
if (!response?.data) {
throw new InternalServerError({
message: "Failed to get organizations: Response was empty"
}); });
} }

View File

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

View File

@@ -0,0 +1,3 @@
export enum WindmillConnectionMethod {
AccessToken = "access-token"
}

View File

@@ -0,0 +1,65 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { WindmillConnectionMethod } from "./windmill-connection-enums";
import { TWindmillConnection, TWindmillConnectionConfig, TWindmillWorkspace } from "./windmill-connection-types";
export const getWindmillInstanceUrl = async (config: TWindmillConnectionConfig) => {
const instanceUrl = config.credentials.instanceUrl
? removeTrailingSlash(config.credentials.instanceUrl)
: "https://app.windmill.dev";
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export const getWindmillConnectionListItem = () => {
return {
name: "Windmill" as const,
app: AppConnection.Windmill as const,
methods: Object.values(WindmillConnectionMethod) as [WindmillConnectionMethod.AccessToken]
};
};
export const validateWindmillConnectionCredentials = async (config: TWindmillConnectionConfig) => {
const instanceUrl = await getWindmillInstanceUrl(config);
const { accessToken } = config.credentials;
try {
await request.get(`${instanceUrl}/api/workspaces/list`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};
export const listWindmillWorkspaces = async (appConnection: TWindmillConnection) => {
const instanceUrl = await getWindmillInstanceUrl(appConnection);
const { accessToken } = appConnection.credentials;
const resp = await request.get<TWindmillWorkspace[]>(`${instanceUrl}/api/workspaces/list`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return resp.data.filter((workspace) => !workspace.deleted);
};

View File

@@ -0,0 +1,70 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { WindmillConnectionMethod } from "./windmill-connection-enums";
export const WindmillConnectionAccessTokenCredentialsSchema = z.object({
accessToken: z
.string()
.trim()
.min(1, "Access Token required")
.describe(AppConnections.CREDENTIALS.WINDMILL.accessToken),
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.optional()
.describe(AppConnections.CREDENTIALS.WINDMILL.instanceUrl)
});
const BaseWindmillConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Windmill) });
export const WindmillConnectionSchema = BaseWindmillConnectionSchema.extend({
method: z.literal(WindmillConnectionMethod.AccessToken),
credentials: WindmillConnectionAccessTokenCredentialsSchema
});
export const SanitizedWindmillConnectionSchema = z.discriminatedUnion("method", [
BaseWindmillConnectionSchema.extend({
method: z.literal(WindmillConnectionMethod.AccessToken),
credentials: WindmillConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateWindmillConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(WindmillConnectionMethod.AccessToken)
.describe(AppConnections.CREATE(AppConnection.Windmill).method),
credentials: WindmillConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Windmill).credentials
)
})
]);
export const CreateWindmillConnectionSchema = ValidateWindmillConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Windmill)
);
export const UpdateWindmillConnectionSchema = z
.object({
credentials: WindmillConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Windmill).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Windmill));
export const WindmillConnectionListItemSchema = z.object({
name: z.literal("Windmill"),
app: z.literal(AppConnection.Windmill),
methods: z.nativeEnum(WindmillConnectionMethod).array()
});

View File

@@ -0,0 +1,28 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listWindmillWorkspaces } from "./windmill-connection-fns";
import { TWindmillConnection } from "./windmill-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TWindmillConnection>;
export const windmillConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Windmill, connectionId, actor);
try {
const workspaces = await listWindmillWorkspaces(appConnection);
return workspaces;
} catch (error) {
return [];
}
};
return {
listWorkspaces
};
};

View File

@@ -0,0 +1,27 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateWindmillConnectionSchema,
ValidateWindmillConnectionCredentialsSchema,
WindmillConnectionSchema
} from "./windmill-connection-schemas";
export type TWindmillConnection = z.infer<typeof WindmillConnectionSchema>;
export type TWindmillConnectionInput = z.infer<typeof CreateWindmillConnectionSchema> & {
app: AppConnection.Windmill;
};
export type TValidateWindmillConnectionCredentialsSchema = typeof ValidateWindmillConnectionCredentialsSchema;
export type TWindmillConnectionConfig = DiscriminativePick<
TWindmillConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TWindmillWorkspace = { id: string; name: string; deleted: boolean };

View File

@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex"; import { Knex } from "knex";
import { TUsers, UserDeviceSchema } from "@app/db/schemas"; import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
@@ -174,20 +174,25 @@ export const authLoginServiceFactory = ({
const userEnc = await userDAL.findUserEncKeyByUsername({ const userEnc = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if ( if (
serverCfg.enabledLoginMethods && serverCfg.enabledLoginMethods &&
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
!providerAuthToken !providerAuthToken
) { ) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with email is disabled by administrator." const userOrgs = await orgDAL.findAllOrgsByUserId(userEnc.userId);
}); if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
} throw new BadRequestError({
message: "Login with email is disabled by administrator."
if (!userEnc || (userEnc && !userEnc.isAccepted)) { });
throw new Error("Failed to find user"); }
} }
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
@@ -573,28 +578,40 @@ export const authLoginServiceFactory = ({
switch (authMethod) { switch (authMethod) {
case AuthMethod.GITHUB: { case AuthMethod.GITHUB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with Github is disabled by administrator.", const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
name: "Oauth 2 login" if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
}); throw new BadRequestError({
message: "Login with Github is disabled by administrator.",
name: "Oauth 2 login"
});
}
} }
break; break;
} }
case AuthMethod.GOOGLE: { case AuthMethod.GOOGLE: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with Google is disabled by administrator.", const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
name: "Oauth 2 login" if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
}); throw new BadRequestError({
message: "Login with Google is disabled by administrator.",
name: "Oauth 2 login"
});
}
} }
break; break;
} }
case AuthMethod.GITLAB: { case AuthMethod.GITLAB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with Gitlab is disabled by administrator.", const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
name: "Oauth 2 login" if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
}); throw new BadRequestError({
message: "Login with Gitlab is disabled by administrator.",
name: "Oauth 2 login"
});
}
} }
break; break;
} }

View File

@@ -195,6 +195,7 @@ export const getIntegrationOptions = async () => {
{ {
name: "AWS Secrets Manager", name: "AWS Secrets Manager",
slug: "aws-secret-manager", slug: "aws-secret-manager",
syncSlug: "aws-secrets-manager",
image: "Amazon Web Services.png", image: "Amazon Web Services.png",
isAvailable: true, isAvailable: true,
type: "custom", type: "custom",

View File

@@ -288,11 +288,6 @@ export const kmsServiceFactory = ({
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
} }
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
});
if (kmsDoc.externalKms) { if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns; let externalKms: TExternalKmsProviderFns;
@@ -353,6 +348,11 @@ export const kmsServiceFactory = ({
}; };
} }
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
});
// internal KMS // internal KMS
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const dataCipher = symmetricCipherService(encryptionAlgorithm); const dataCipher = symmetricCipherService(encryptionAlgorithm);
@@ -509,11 +509,6 @@ export const kmsServiceFactory = ({
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
} }
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
});
if (kmsDoc.externalKms) { if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns; let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
@@ -568,6 +563,11 @@ export const kmsServiceFactory = ({
}; };
} }
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
});
// internal KMS // internal KMS
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const dataCipher = symmetricCipherService(encryptionAlgorithm); const dataCipher = symmetricCipherService(encryptionAlgorithm);

View File

@@ -96,7 +96,9 @@ export const orgDALFactory = (db: TDbClient) => {
}; };
// special query // special query
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => { const findAllOrgsByUserId = async (
userId: string
): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => {
try { try {
const org = (await db const org = (await db
.replicaNode()(TableName.OrgMembership) .replicaNode()(TableName.OrgMembership)
@@ -117,6 +119,7 @@ export const orgDALFactory = (db: TDbClient) => {
); );
}) })
.select(selectAllTableCols(TableName.Organization)) .select(selectAllTableCols(TableName.Organization))
.select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole"))
.select( .select(
db.raw(` db.raw(`
CASE CASE
@@ -125,7 +128,7 @@ export const orgDALFactory = (db: TDbClient) => {
ELSE '' ELSE ''
END as "orgAuthMethod" END as "orgAuthMethod"
`) `)
)) as (TOrganizations & { orgAuthMethod: string })[]; )) as (TOrganizations & { orgAuthMethod: string; userRole: string })[];
return org; return org;
} catch (error) { } catch (error) {

View File

@@ -16,5 +16,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
allowSecretSharingOutsideOrganization: true, allowSecretSharingOutsideOrganization: true,
shouldUseNewPrivilegeSystem: true, shouldUseNewPrivilegeSystem: true,
privilegeUpgradeInitiatedByUsername: true, privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true privilegeUpgradeInitiatedAt: true,
bypassOrgAuthEnabled: true
}); });

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