From 93218d5a3ffae289833581ffd3dcce570e2bbdb2 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 18 Oct 2024 16:03:36 +0530 Subject: [PATCH] Revert "Revert "Permission phase 2"" This reverts commit 8b9244b079592ded3ce46f1c92faa68fd81eebe0. --- .../routes/v3/secret-reference.spec.ts | 22 +- backend/package-lock.json | 614 ++++++++------ .../20240925100349_managed-secret-sharing.ts | 25 +- ...20241003220151_kms-key-cmek-alterations.ts | 12 +- ...20241008172622_project-permission-split.ts | 101 +++ ...ity-project-additional-privilege-router.ts | 15 +- .../src/ee/routes/v1/project-role-router.ts | 15 +- .../v1/user-additional-privilege-router.ts | 53 +- backend/src/ee/routes/v2/index.ts | 11 + .../src/ee/routes/v2/project-role-router.ts | 272 ++++++ .../access-approval-policy-dal.ts | 2 +- .../access-approval-policy-fns.ts | 36 - .../access-approval-policy-service.ts | 72 -- .../access-approval-request-service.ts | 18 - .../dynamic-secret-lease-service.ts | 25 +- .../dynamic-secret/dynamic-secret-service.ts | 41 +- ...ty-project-additional-privilege-service.ts | 14 +- .../services/permission/permission-service.ts | 4 +- .../services/permission/permission-types.ts | 4 +- .../services/permission/project-permission.ts | 289 ++++++- ...oject-user-additional-privilege-service.ts | 47 +- .../secret-approval-policy-dal.ts | 2 +- .../secret-approval-policy-service.ts | 15 +- .../secret-approval-request-service.ts | 33 +- .../secret-replication-service.ts | 16 +- .../secret-rotation-service.ts | 5 +- backend/src/lib/casl/knex.ts | 111 --- backend/src/lib/fn/array.ts | 22 + backend/src/lib/knex/dynamic.ts | 52 +- backend/src/lib/knex/index.ts | 7 +- backend/src/server/plugins/error-handler.ts | 2 +- backend/src/server/routes/index.ts | 9 +- backend/src/server/routes/sanitizedSchemas.ts | 3 +- .../routes/santizedSchemas/permission.ts | 11 + .../src/server/routes/v1/dashboard-router.ts | 25 +- .../external-migration-fns.ts | 4 +- .../integration-delete-secret.ts | 3 +- .../integration/integration-service.ts | 10 +- .../project-role/project-role-service.ts | 2 +- .../secret-folder/secret-folder-fns.ts | 6 - .../secret-folder/secret-folder-service.ts | 73 +- .../secret-import/secret-import-fns.ts | 178 ++-- .../secret-import/secret-import-service.ts | 71 +- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 104 ++- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 61 +- .../secret-v2-bridge-service.ts | 790 ++++++++++++------ .../secret-v2-bridge-types.ts | 6 + backend/src/services/secret/secret-fns.ts | 6 +- backend/src/services/secret/secret-queue.ts | 7 +- backend/src/services/secret/secret-service.ts | 15 +- .../permissions/GlobPermissionInfo.tsx | 33 + .../permissions/ProjectPermissionCan.tsx | 26 +- frontend/src/components/permissions/index.tsx | 1 + frontend/src/components/v2/Select/Select.tsx | 4 +- .../ProjectPermissionContext/index.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 66 +- frontend/src/context/index.tsx | 1 + .../withProjectPermission.tsx | 30 +- frontend/src/hooks/api/dashboard/queries.tsx | 13 +- frontend/src/hooks/api/dashboard/types.ts | 3 + .../mutation.tsx | 8 +- .../queries.tsx | 8 +- .../projectUserAdditionalPrivilege/types.tsx | 17 +- frontend/src/hooks/api/roles/mutation.tsx | 6 +- frontend/src/hooks/api/roles/queries.tsx | 34 +- frontend/src/hooks/api/roles/types.ts | 1 + frontend/src/lib/fn/array.ts | 19 + frontend/src/lib/fn/object.ts | 20 + .../SpecificPrivilegeSection.tsx | 30 +- .../ProjectRoleModifySection.utils.ts | 208 +++-- .../RolePermissionsSection.tsx | 23 +- .../GeneralPermissionConditions.tsx | 176 ++++ ...ions.tsx => GeneralPermissionPolicies.tsx} | 60 +- .../components/NewPermissionRule.tsx | 3 +- .../components/PermissionConditionHelpers.tsx | 32 + .../components/SecretPermissionConditions.tsx | 70 +- .../views/SecretMainPage/SecretMainPage.tsx | 148 ++-- .../components/ActionBar/ActionBar.tsx | 50 +- .../DynamicSecretLease.tsx | 34 +- .../DynamicSecretListView.tsx | 40 +- .../FolderListView/FolderListView.tsx | 21 +- .../SecretDropzone/CopySecretsFromBoard.tsx | 7 +- .../SecretDropzone/SecretDropzone.tsx | 14 +- .../SecretImportListView/SecretImportItem.tsx | 11 +- .../SecretListView/SecretDetaiSidebar.tsx | 76 +- .../components/SecretListView/SecretItem.tsx | 61 +- .../SecretListView/SecretListView.tsx | 11 - .../SecretListView/SecretListView.utils.ts | 7 +- .../SecretListView/SecretNoAccessListView.tsx | 49 ++ .../components/SecretListView/index.tsx | 1 + .../SecretOverviewPage/SecretOverviewPage.tsx | 45 +- .../CreateSecretForm/CreateSecretForm.tsx | 29 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 25 +- .../SecretNoAccessOverviewTableRow.tsx | 51 ++ .../SecretRenameRow.tsx | 19 +- .../SecretOverviewTableRow/index.tsx | 1 + .../SelectionPanel/SelectionPanel.tsx | 56 +- .../DeleteProjectSection.tsx | 5 +- .../EncryptionTab/EncryptionTab.tsx | 5 +- .../ProjectNameChangeSection.tsx | 5 +- 100 files changed, 3390 insertions(+), 1615 deletions(-) create mode 100644 backend/src/db/migrations/20241008172622_project-permission-split.ts create mode 100644 backend/src/ee/routes/v2/index.ts create mode 100644 backend/src/ee/routes/v2/project-role-router.ts delete mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts delete mode 100644 backend/src/lib/casl/knex.ts create mode 100644 backend/src/server/routes/santizedSchemas/permission.ts delete mode 100644 backend/src/services/secret-folder/secret-folder-fns.ts create mode 100644 frontend/src/components/permissions/GlobPermissionInfo.tsx create mode 100644 frontend/src/lib/fn/object.ts create mode 100644 frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx rename frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/{GeneralPermissionOptions.tsx => GeneralPermissionPolicies.tsx} (66%) create mode 100644 frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/PermissionConditionHelpers.tsx create mode 100644 frontend/src/views/SecretMainPage/components/SecretListView/SecretNoAccessListView.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretNoAccessOverviewTableRow.tsx diff --git a/backend/e2e-test/routes/v3/secret-reference.spec.ts b/backend/e2e-test/routes/v3/secret-reference.spec.ts index fb23cdc25..560565342 100644 --- a/backend/e2e-test/routes/v3/secret-reference.spec.ts +++ b/backend/e2e-test/routes/v3/secret-reference.spec.ts @@ -56,7 +56,10 @@ describe("Secret expansion", () => { } ]; - await Promise.all(secrets.map((el) => createSecretV2(el))); + for (const secret of secrets) { + // eslint-disable-next-line no-await-in-loop + await createSecretV2(secret); + } const expandedSecret = await getSecretByNameV2({ environmentSlug: seedData1.environment.slug, @@ -123,7 +126,10 @@ describe("Secret expansion", () => { } ]; - await Promise.all(secrets.map((el) => createSecretV2(el))); + for (const secret of secrets) { + // eslint-disable-next-line no-await-in-loop + await createSecretV2(secret); + } const expandedSecret = await getSecretByNameV2({ environmentSlug: seedData1.environment.slug, @@ -190,7 +196,11 @@ describe("Secret expansion", () => { } ]; - await Promise.all(secrets.map((el) => createSecretV2(el))); + for (const secret of secrets) { + // eslint-disable-next-line no-await-in-loop + await createSecretV2(secret); + } + const secretImportFromProdToDev = await createSecretImport({ environmentSlug: seedData1.environment.slug, workspaceId: projectId, @@ -275,7 +285,11 @@ describe("Secret expansion", () => { } ]; - await Promise.all(secrets.map((el) => createSecretV2(el))); + for (const secret of secrets) { + // eslint-disable-next-line no-await-in-loop + await createSecretV2(secret); + } + const secretImportFromProdToDev = await createSecretImport({ environmentSlug: seedData1.environment.slug, workspaceId: projectId, diff --git a/backend/package-lock.json b/backend/package-lock.json index dbd8c8073..7d1f95ad8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -3827,13 +3827,14 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -5851,9 +5852,10 @@ } }, "node_modules/@probot/pino": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-2.3.5.tgz", - "integrity": "sha512-IiyiNZonMw1dHC4EAdD55y5owV733d9Gll/IKsrLikB7EJ54+eMCOtL/qo+OmgWN9XV3NTDfziEQF2og/OBKog==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-2.4.0.tgz", + "integrity": "sha512-KUJ3eK2zLrPny7idWm9eQbBNhCJUjm1A1ttA6U4qiR2/ONWSffVlvr8oR26L59sVhoDkv1DOGmGPZS/bvSFisw==", + "license": "MIT", "dependencies": { "@sentry/node": "^6.0.0", "pino-pretty": "^6.0.0", @@ -5922,208 +5924,224 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz", - "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz", - "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", - "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz", - "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz", - "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz", - "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz", - "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz", - "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz", - "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz", - "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz", - "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz", - "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz", - "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz", - "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz", - "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz", - "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -7281,10 +7299,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.21", @@ -8533,11 +8552,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" - }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -8847,36 +8861,15 @@ } }, "node_modules/avvio": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.1.tgz", - "integrity": "sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", "dependencies": { - "archy": "^1.0.0", - "debug": "^4.0.0", - "fastq": "^1.6.1" + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" } }, - "node_modules/avvio/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/avvio/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/aws-sdk": { "version": "2.1553.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1553.0.tgz", @@ -9735,9 +9728,10 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10929,15 +10923,6 @@ "node": ">= 0.8.0" } }, - "node_modules/express-session/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -10959,15 +10944,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "peer": true }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -11119,9 +11095,9 @@ } }, "node_modules/fastify": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.0.tgz", - "integrity": "sha512-Fq/7ziWKc6pYLYLIlCRaqJqEVTIZ5tZYfcW/mDK2AQ9v/sqjGFpj0On0/7hU50kbPVjLO4de+larPA1WwPZSfw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.28.1.tgz", + "integrity": "sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==", "funding": [ { "type": "github", @@ -11132,17 +11108,18 @@ "url": "https://opencollective.com/fastify" } ], + "license": "MIT", "dependencies": { "@fastify/ajv-compiler": "^3.5.0", "@fastify/error": "^3.4.0", "@fastify/fast-json-stringify-compiler": "^4.3.0", "abstract-logging": "^2.0.1", - "avvio": "^8.2.1", + "avvio": "^8.3.0", "fast-content-type-parse": "^1.1.0", "fast-json-stringify": "^5.8.0", "find-my-way": "^8.0.0", "light-my-request": "^5.11.0", - "pino": "^8.17.0", + "pino": "^9.0.0", "process-warning": "^3.0.0", "proxy-addr": "^2.0.7", "rfdc": "^1.3.0", @@ -11156,15 +11133,78 @@ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" }, + "node_modules/fastify/node_modules/pino": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz", + "integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/fastify/node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/fastify/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/fastify/node_modules/pino/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" + }, "node_modules/fastify/node_modules/process-warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, + "node_modules/fastify/node_modules/sonic-boom": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz", + "integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/fastify/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -11227,13 +11267,14 @@ "license": "MIT" }, "node_modules/find-my-way": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", - "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", - "safe-regex2": "^2.0.0" + "safe-regex2": "^3.1.0" }, "engines": { "node": ">=14" @@ -13380,15 +13421,22 @@ } }, "node_modules/light-my-request": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", - "integrity": "sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.13.0.tgz", + "integrity": "sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==", + "license": "BSD-3-Clause", "dependencies": { - "cookie": "^0.5.0", - "process-warning": "^2.0.0", + "cookie": "^0.6.0", + "process-warning": "^3.0.0", "set-cookie-parser": "^2.4.1" } }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", @@ -15213,10 +15261,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "3.0.1", @@ -15493,9 +15542,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -15511,10 +15560,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -16340,11 +16390,12 @@ } }, "node_modules/ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=10" } }, "node_modules/retry": { @@ -16443,12 +16494,13 @@ } }, "node_modules/rollup": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", - "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -16458,22 +16510,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.3", - "@rollup/rollup-android-arm64": "4.14.3", - "@rollup/rollup-darwin-arm64": "4.14.3", - "@rollup/rollup-darwin-x64": "4.14.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.3", - "@rollup/rollup-linux-arm-musleabihf": "4.14.3", - "@rollup/rollup-linux-arm64-gnu": "4.14.3", - "@rollup/rollup-linux-arm64-musl": "4.14.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3", - "@rollup/rollup-linux-riscv64-gnu": "4.14.3", - "@rollup/rollup-linux-s390x-gnu": "4.14.3", - "@rollup/rollup-linux-x64-gnu": "4.14.3", - "@rollup/rollup-linux-x64-musl": "4.14.3", - "@rollup/rollup-win32-arm64-msvc": "4.14.3", - "@rollup/rollup-win32-ia32-msvc": "4.14.3", - "@rollup/rollup-win32-x64-msvc": "4.14.3", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -16565,11 +16617,12 @@ } }, "node_modules/safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", "dependencies": { - "ret": "~0.2.0" + "ret": "~0.4.0" } }, "node_modules/safe-stable-stringify": { @@ -16938,10 +16991,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -18811,14 +18865,15 @@ } }, "node_modules/vite": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz", - "integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -18837,6 +18892,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -18854,6 +18910,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -18911,13 +18970,14 @@ "dev": true }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -18927,13 +18987,14 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -18943,13 +19004,14 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -18959,13 +19021,14 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -18975,13 +19038,14 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -18991,13 +19055,14 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -19007,13 +19072,14 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -19023,13 +19089,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19039,13 +19106,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19055,13 +19123,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19071,13 +19140,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19087,13 +19157,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19103,13 +19174,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19119,13 +19191,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19135,13 +19208,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19151,13 +19225,14 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -19167,13 +19242,14 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -19183,13 +19259,14 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -19199,13 +19276,14 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -19215,13 +19293,14 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -19231,13 +19310,14 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -19247,13 +19327,14 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -19263,11 +19344,12 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -19275,29 +19357,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/vitest": { diff --git a/backend/src/db/migrations/20240925100349_managed-secret-sharing.ts b/backend/src/db/migrations/20240925100349_managed-secret-sharing.ts index 56784d314..f64d7f858 100644 --- a/backend/src/db/migrations/20240925100349_managed-secret-sharing.ts +++ b/backend/src/db/migrations/20240925100349_managed-secret-sharing.ts @@ -4,27 +4,40 @@ import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.SecretSharing)) { + const hasEncryptedSecret = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSecret"); + const hasIdentifier = await knex.schema.hasColumn(TableName.SecretSharing, "identifier"); + await knex.schema.alterTable(TableName.SecretSharing, (t) => { t.string("iv").nullable().alter(); t.string("tag").nullable().alter(); t.string("encryptedValue").nullable().alter(); - t.binary("encryptedSecret").nullable(); + if (!hasEncryptedSecret) { + t.binary("encryptedSecret").nullable(); + } t.string("hashedHex").nullable().alter(); - t.string("identifier", 64).nullable(); - t.unique("identifier"); - t.index("identifier"); + if (!hasIdentifier) { + t.string("identifier", 64).nullable(); + t.unique("identifier"); + t.index("identifier"); + } }); } } export async function down(knex: Knex): Promise { + const hasEncryptedSecret = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSecret"); + const hasIdentifier = await knex.schema.hasColumn(TableName.SecretSharing, "identifier"); if (await knex.schema.hasTable(TableName.SecretSharing)) { await knex.schema.alterTable(TableName.SecretSharing, (t) => { - t.dropColumn("encryptedSecret"); + if (hasEncryptedSecret) { + t.dropColumn("encryptedSecret"); + } - t.dropColumn("identifier"); + if (hasIdentifier) { + t.dropColumn("identifier"); + } }); } } diff --git a/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts b/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts index 6b701eea4..bdad443c9 100644 --- a/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts +++ b/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts @@ -7,15 +7,18 @@ export async function up(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.KmsKey)) { const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId"); // drop constraint if exists (won't exist if rolled back, see below) await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex); // projectId for CMEK functionality await knex.schema.alterTable(TableName.KmsKey, (table) => { - table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE"); + if (!hasProjectId) { + table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE"); + } - if (hasOrgId) { + if (hasOrgId && hasSlug) { table.unique(["orgId", "projectId", "slug"]); } @@ -30,6 +33,7 @@ export async function down(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.KmsKey)) { const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name"); + const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId"); // remove projectId for CMEK functionality await knex.schema.alterTable(TableName.KmsKey, (table) => { @@ -40,7 +44,9 @@ export async function down(knex: Knex): Promise { if (hasOrgId) { table.dropUnique(["orgId", "projectId", "slug"]); } - table.dropColumn("projectId"); + if (hasProjectId) { + table.dropColumn("projectId"); + } }); } } diff --git a/backend/src/db/migrations/20241008172622_project-permission-split.ts b/backend/src/db/migrations/20241008172622_project-permission-split.ts new file mode 100644 index 000000000..3b322b18f --- /dev/null +++ b/backend/src/db/migrations/20241008172622_project-permission-split.ts @@ -0,0 +1,101 @@ +/* eslint-disable no-await-in-loop */ +import { packRules, unpackRules } from "@casl/ability/extra"; +import { Knex } from "knex"; + +import { + backfillPermissionV1SchemaToV2Schema, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; + +import { TableName } from "../schemas"; + +const CHUNK_SIZE = 1000; +export async function up(knex: Knex): Promise { + const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version"); + if (!hasVersion) { + await knex.schema.alterTable(TableName.ProjectRoles, (t) => { + t.integer("version").defaultTo(1).notNullable(); + }); + + const docs = await knex(TableName.ProjectRoles).select("*"); + const updatedDocs = docs + .filter((i) => { + const permissionString = JSON.stringify(i.permissions || []); + return ( + !permissionString.includes(ProjectPermissionSub.SecretImports) && + !permissionString.includes(ProjectPermissionSub.DynamicSecrets) + ); + }) + .map((el) => ({ + ...el, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions)))) + })); + if (updatedDocs.length) { + for (let i = 0; i < updatedDocs.length; i += CHUNK_SIZE) { + const chunk = updatedDocs.slice(i, i + CHUNK_SIZE); + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(); + } + } + + // secret permission is split into multiple ones like secrets, folders, imports and dynamic-secrets + // so we just find all the privileges with respective mapping and map it as needed + const identityPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select("*"); + const updatedIdentityPrivilegesDocs = identityPrivileges + .filter((i) => { + const permissionString = JSON.stringify(i.permissions || []); + return ( + !permissionString.includes(ProjectPermissionSub.SecretImports) && + !permissionString.includes(ProjectPermissionSub.DynamicSecrets) && + !permissionString.includes(ProjectPermissionSub.SecretFolders) + ); + }) + .map((el) => ({ + ...el, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions)))) + })); + if (updatedIdentityPrivilegesDocs.length) { + for (let i = 0; i < updatedIdentityPrivilegesDocs.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityPrivilegesDocs.slice(i, i + CHUNK_SIZE); + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(); + } + } + + const userPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select("*"); + const updatedUserPrivilegeDocs = userPrivileges + .filter((i) => { + const permissionString = JSON.stringify(i.permissions || []); + return ( + !permissionString.includes(ProjectPermissionSub.SecretImports) && + !permissionString.includes(ProjectPermissionSub.DynamicSecrets) && + !permissionString.includes(ProjectPermissionSub.SecretFolders) + ); + }) + .map((el) => ({ + ...el, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions)))) + })); + if (docs.length) { + for (let i = 0; i < updatedUserPrivilegeDocs.length; i += CHUNK_SIZE) { + const chunk = updatedUserPrivilegeDocs.slice(i, i + CHUNK_SIZE); + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(); + } + } + } +} + +export async function down(knex: Knex): Promise { + const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version"); + if (hasVersion) { + await knex.schema.alterTable(TableName.ProjectRoles, (t) => { + t.dropColumn("version"); + }); + + // permission change can be ignored + } +} diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index 2e7fd60d3..91a4402fb 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -4,6 +4,7 @@ import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -79,7 +80,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, - permissions: JSON.stringify(packRules(permission)) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) }); return { privilege }; } @@ -159,7 +162,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: true, - permissions: JSON.stringify(packRules(permission)) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) }); return { privilege }; } @@ -244,7 +249,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F projectSlug: req.body.projectSlug, data: { ...updatedInfo, - permissions: permission ? JSON.stringify(packRules(permission)) : undefined + permissions: permission + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) + : undefined } }); return { privilege }; diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index 392046757..9edd29030 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -3,7 +3,10 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; -import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission"; +import { + backfillPermissionV1SchemaToV2Schema, + ProjectPermissionV1Schema +} from "@app/ee/services/permission/project-permission"; import { PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -43,7 +46,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { .describe(PROJECT_ROLE.CREATE.slug), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description), - permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.CREATE.permissions) + permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions) }), response: { 200: z.object({ @@ -61,7 +64,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { projectSlug: req.params.projectSlug, data: { ...req.body, - permissions: JSON.stringify(packRules(req.body.permissions)) + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) } }); return { role }; @@ -103,7 +106,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description), - permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() }), response: { 200: z.object({ @@ -122,7 +125,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: req.params.roleId, data: { ...req.body, - permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined + permissions: req.body.permissions + ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) + : undefined } }); return { role }; diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index 7225caecf..1ba7e0e33 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,13 +1,16 @@ +import { packRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas"; +import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ProjectSpecificPrivilegePermissionSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { @@ -31,7 +34,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }) .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) + permissions: ProjectSpecificPrivilegePermissionSchema.describe( + PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions + ) }), response: { 200: z.object({ @@ -49,7 +54,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, - permissions: JSON.stringify(req.body.permissions) + permissions: JSON.stringify( + packRules( + backfillPermissionV1SchemaToV2Schema( + req.body.permissions.actions.map((action) => ({ + action, + subject: req.body.permissions.subject, + conditions: req.body.permissions.conditions + })) + ) + ) + ) }); return { privilege }; } @@ -75,7 +90,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }) .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + permissions: ProjectSpecificPrivilegePermissionSchema.describe( + PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions + ), temporaryMode: z .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), @@ -104,7 +121,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr ...req.body, slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`, isTemporary: true, - permissions: JSON.stringify(req.body.permissions) + permissions: JSON.stringify( + packRules( + backfillPermissionV1SchemaToV2Schema( + req.body.permissions.actions.map((action) => ({ + action, + subject: req.body.permissions.subject, + conditions: req.body.permissions.conditions + })) + ) + ) + ) }); return { privilege }; } @@ -131,7 +158,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr message: "Slug must be a valid slug" }) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), - permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + permissions: ProjectSpecificPrivilegePermissionSchema.describe( + PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions + ).optional(), isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) @@ -160,7 +189,19 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined, + permissions: req.body.permissions + ? JSON.stringify( + packRules( + backfillPermissionV1SchemaToV2Schema( + req.body.permissions.actions.map((action) => ({ + action, + subject: req.body.permissions!.subject, + conditions: req.body.permissions!.conditions + })) + ) + ) + ) + : undefined, privilegeId: req.params.privilegeId }); return { privilege }; diff --git a/backend/src/ee/routes/v2/index.ts b/backend/src/ee/routes/v2/index.ts new file mode 100644 index 000000000..980e4777b --- /dev/null +++ b/backend/src/ee/routes/v2/index.ts @@ -0,0 +1,11 @@ +import { registerProjectRoleRouter } from "./project-role-router"; + +export const registerV2EERoutes = async (server: FastifyZodProvider) => { + // org role starts with organization + await server.register( + async (projectRouter) => { + await projectRouter.register(registerProjectRoleRouter); + }, + { prefix: "/workspace" } + ); +}; diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts new file mode 100644 index 000000000..86d64141d --- /dev/null +++ b/backend/src/ee/routes/v2/project-role-router.ts @@ -0,0 +1,272 @@ +import { packRules } from "@casl/ability/extra"; +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; +import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; +import { PROJECT_ROLE } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/:projectSlug/roles", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create a project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug) + }), + body: z.object({ + slug: z + .string() + .toLowerCase() + .trim() + .min(1) + .refine( + (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), + "Please choose a different slug, the slug you have entered is reserved" + ) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid" + }) + .describe(PROJECT_ROLE.CREATE.slug), + name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), + description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description), + permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions) + }), + response: { + 200: z.object({ + role: SanitizedRoleSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.createRole({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + projectSlug: req.params.projectSlug, + data: { + ...req.body, + permissions: JSON.stringify(packRules(req.body.permissions)) + } + }); + return { role }; + } + }); + + server.route({ + method: "PATCH", + url: "/:projectSlug/roles/:roleId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update a project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug), + roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId) + }), + body: z.object({ + slug: z + .string() + .toLowerCase() + .trim() + .optional() + .describe(PROJECT_ROLE.UPDATE.slug) + .refine( + (val) => + typeof val === "undefined" || + !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), + "Please choose a different slug, the slug you have entered is reserved" + ) + .refine((val) => typeof val === "undefined" || slugify(val) === val, { + message: "Slug must be a valid" + }), + name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), + description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description), + permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + }), + response: { + 200: z.object({ + role: SanitizedRoleSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.updateRole({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + projectSlug: req.params.projectSlug, + roleId: req.params.roleId, + data: { + ...req.body, + permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined + } + }); + return { role }; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectSlug/roles/:roleId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete a project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug), + roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId) + }), + response: { + 200: z.object({ + role: SanitizedRoleSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.deleteRole({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + projectSlug: req.params.projectSlug, + roleId: req.params.roleId + }); + return { role }; + } + }); + + server.route({ + method: "GET", + url: "/:projectSlug/roles", + config: { + rateLimit: readLimit + }, + schema: { + description: "List project role", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug) + }), + response: { + 200: z.object({ + roles: ProjectRolesSchema.omit({ permissions: true }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const roles = await server.services.projectRole.listRoles({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + projectSlug: req.params.projectSlug + }); + return { roles }; + } + }); + + server.route({ + method: "GET", + url: "/:projectSlug/roles/slug/:roleSlug", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug), + roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug) + }), + response: { + 200: z.object({ + role: SanitizedRoleSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const role = await server.services.projectRole.getRoleBySlug({ + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + projectSlug: req.params.projectSlug, + roleSlug: req.params.roleSlug + }); + return { role }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/permissions", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + data: z.object({ + membership: ProjectMembershipsSchema.extend({ + roles: z + .object({ + role: z.string() + }) + .array() + }), + permissions: z.any().array() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { permissions, membership } = await server.services.projectRole.getUserPermission( + req.permission.id, + req.params.projectId, + req.permission.authMethod, + req.permission.orgId + ); + + return { data: { permissions, membership } }; + } + }); +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 569580507..220701410 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -14,7 +14,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const accessApprovalPolicyFindQuery = async ( tx: Knex, - filter: TFindFilter, + filter: TFindFilter, customFilter?: { policyId?: string; } diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts deleted file mode 100644 index 0e242f04c..000000000 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ForbiddenError, subject } from "@casl/ability"; - -import { ActorType } from "@app/services/auth/auth-type"; - -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; -import { TIsApproversValid } from "./access-approval-policy-types"; - -export const isApproversValid = async ({ - userIds, - projectId, - orgId, - envSlug, - actorAuthMethod, - secretPath, - permissionService -}: TIsApproversValid) => { - try { - for await (const userId of userIds) { - const { permission: approverPermission } = await permissionService.getProjectPermission( - ActorType.USER, - userId, - projectId, - actorAuthMethod, - orgId - ); - - ForbiddenError.from(approverPermission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath }) - ); - } - } catch { - return false; - } - return true; -}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 715b88ecf..ee7cf2572 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -11,7 +11,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TGroupDALFactory } from "../group/group-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; -import { isApproversValid } from "./access-approval-policy-fns"; import { ApproverType, TCreateAccessApprovalPolicy, @@ -134,22 +133,6 @@ export const accessApprovalPolicyServiceFactory = ({ .map((user) => user.id); verifyAllApprovers.push(...verifyGroupApprovers); - const approversValid = await isApproversValid({ - projectId: project.id, - orgId: actorOrgId, - envSlug: environment, - secretPath, - actorAuthMethod, - permissionService, - userIds: verifyAllApprovers - }); - - if (!approversValid) { - throw new BadRequestError({ - message: "One or more approvers doesn't have access to be specified secret path" - }); - } - const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const doc = await accessApprovalPolicyDAL.create( { @@ -293,22 +276,6 @@ export const accessApprovalPolicyServiceFactory = ({ userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id)); } - const approversValid = await isApproversValid({ - projectId: accessApprovalPolicy.projectId, - orgId: actorOrgId, - envSlug: accessApprovalPolicy.environment.slug, - secretPath: doc.secretPath!, - actorAuthMethod, - permissionService, - userIds: userApproverIds - }); - - if (!approversValid) { - throw new BadRequestError({ - message: "One or more approvers doesn't have access to be specified secret path" - }); - } - await accessApprovalPolicyApproverDAL.insertMany( userApproverIds.map((userId) => ({ approverUserId: userId, @@ -319,45 +286,6 @@ export const accessApprovalPolicyServiceFactory = ({ } if (groupApprovers) { - const usersPromises: Promise< - { - id: string; - email: string | null | undefined; - username: string; - firstName: string | null | undefined; - lastName: string | null | undefined; - isPartOfGroup: boolean; - }[] - >[] = []; - - for (const groupId of groupApprovers) { - usersPromises.push( - groupDAL - .findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }) - .then((group) => group.members) - ); - } - const verifyGroupApprovers = (await Promise.all(usersPromises)) - .flat() - .filter((user) => user.isPartOfGroup) - .map((user) => user.id); - - const approversValid = await isApproversValid({ - projectId: accessApprovalPolicy.projectId, - orgId: actorOrgId, - envSlug: accessApprovalPolicy.environment.slug, - secretPath: doc.secretPath!, - actorAuthMethod, - permissionService, - userIds: verifyGroupApprovers - }); - - if (!approversValid) { - throw new BadRequestError({ - message: "One or more approvers doesn't have access to be specified secret path" - }); - } - await accessApprovalPolicyApproverDAL.insertMany( groupApprovers.map((groupId) => ({ approverGroupId: groupId, diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 160c77cad..14accff41 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -17,7 +17,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; -import { isApproversValid } from "../access-approval-policy/access-approval-policy-fns"; import { TGroupDALFactory } from "../group/group-dal"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; @@ -78,7 +77,6 @@ export const accessApprovalRequestServiceFactory = ({ permissionService, accessApprovalRequestDAL, accessApprovalRequestReviewerDAL, - projectMembershipDAL, accessApprovalPolicyDAL, accessApprovalPolicyApproverDAL, additionalPrivilegeDAL, @@ -331,22 +329,6 @@ export const accessApprovalRequestServiceFactory = ({ throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" }); } - const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); - - const approversValid = await isApproversValid({ - projectId: accessApprovalRequest.projectId, - orgId: actorOrgId, - envSlug: accessApprovalRequest.environment, - secretPath: accessApprovalRequest.policy.secretPath!, - actorAuthMethod, - permissionService, - userIds: [reviewerProjectMembership.userId] - }); - - if (!approversValid) { - throw new ForbiddenRequestError({ message: "You don't have access to approve this request" }); - } - const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 6fc099993..38d7d1abd 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -4,7 +4,10 @@ import ms from "ms"; import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -72,8 +75,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -151,8 +154,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -230,8 +233,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -299,8 +302,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -341,8 +344,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 2dd4b4fd9..b2c28892d 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -3,7 +3,10 @@ import { ForbiddenError, subject } from "@casl/ability"; import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection } from "@app/lib/types"; @@ -77,8 +80,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.CreateRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -148,8 +151,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.EditRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -231,8 +234,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -291,8 +294,12 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + 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); @@ -340,8 +347,8 @@ export const dynamicSecretServiceFactory = ({ // verify user has access to each env in request environmentSlugs.forEach((environmentSlug) => ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ) ); } @@ -380,8 +387,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -428,8 +435,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -471,8 +478,8 @@ export const dynamicSecretServiceFactory = ({ // verify user has access to each env in request environmentSlugs.forEach((environmentSlug) => ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ) ); } diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 55d567601..185c78b33 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -1,10 +1,10 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, unpackRules } from "@casl/ability/extra"; import ms from "ms"; -import { z } from "zod"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -32,16 +32,6 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< typeof identityProjectAdditionalPrivilegeServiceFactory >; -// TODO(akhilmhdh): move this to more centralized -export const UnpackedPermissionSchema = z.object({ - subject: z - .union([z.string().min(1), z.string().array()]) - .transform((el) => (typeof el !== "string" ? el[0] : el)) - .optional(), - action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), - conditions: z.unknown().optional() -}); - const unpackPermissions = (permissions: unknown) => UnpackedPermissionSchema.array().parse( unpackRules((permissions || []) as PackRule>>[]) @@ -207,7 +197,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); return { ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) }; }; @@ -335,7 +324,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); return identityPrivileges.map((el) => ({ ...el, - permissions: unpackPermissions(el.permissions) })); }; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 900be4b38..e762d00ec 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -67,7 +67,7 @@ export const permissionServiceFactory = ({ throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` }); } }) - .reduce((curr, prev) => prev.concat(curr), []); + .reduce((prev, curr) => prev.concat(curr), []); return createMongoAbility(rules, { conditionsMatcher @@ -98,7 +98,7 @@ export const permissionServiceFactory = ({ }); } }) - .reduce((curr, prev) => prev.concat(curr), []); + .reduce((prev, curr) => prev.concat(curr), []); return rules; }; diff --git a/backend/src/ee/services/permission/permission-types.ts b/backend/src/ee/services/permission/permission-types.ts index 60fcbec85..8df85054d 100644 --- a/backend/src/ee/services/permission/permission-types.ts +++ b/backend/src/ee/services/permission/permission-types.ts @@ -11,8 +11,8 @@ export enum PermissionConditionOperators { } export const PermissionConditionSchema = { - [PermissionConditionOperators.$IN]: z.string().min(1).array(), - [PermissionConditionOperators.$ALL]: z.string().min(1).array(), + [PermissionConditionOperators.$IN]: z.string().trim().min(1).array(), + [PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(), [PermissionConditionOperators.$REGEX]: z .string() .min(1) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index b2b34e488..a3998206d 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,9 +1,8 @@ import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { z } from "zod"; -import { TableName } from "@app/db/schemas"; import { conditionsMatcher } from "@app/lib/casl"; -import { BadRequestError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types"; @@ -23,6 +22,14 @@ export enum ProjectPermissionCmekActions { Decrypt = "decrypt" } +export enum ProjectPermissionDynamicSecretActions { + ReadRootCredential = "read-root-credential", + CreateRootCredential = "create-root-credential", + EditRootCredential = "edit-root-credential", + DeleteRootCredential = "delete-root-credential", + Lease = "lease" +} + export enum ProjectPermissionSub { Role = "role", Member = "member", @@ -38,6 +45,8 @@ export enum ProjectPermissionSub { Project = "workspace", Secrets = "secrets", SecretFolders = "secret-folders", + SecretImports = "secret-imports", + DynamicSecrets = "dynamic-secrets", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", @@ -54,19 +63,8 @@ export enum ProjectPermissionSub { export type SecretSubjectFields = { environment: string; secretPath: string; - // secretName: string; - // secretTags: string[]; -}; - -export const CaslSecretsV2SubjectKnexMapper = (field: string) => { - switch (field) { - case "secretName": - return `${TableName.SecretV2}.key`; - case "secretTags": - return `${TableName.SecretTag}.slug`; - default: - break; - } + secretName?: string; + secretTags?: string[]; }; export type SecretFolderSubjectFields = { @@ -74,6 +72,16 @@ export type SecretFolderSubjectFields = { secretPath: string; }; +export type DynamicSecretSubjectFields = { + environment: string; + secretPath: string; +}; + +export type SecretImportSubjectFields = { + environment: string; + secretPath: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionActions, @@ -86,6 +94,20 @@ export type ProjectPermissionSet = | (ForcedSubject & SecretFolderSubjectFields) ) ] + | [ + ProjectPermissionDynamicSecretActions, + ( + | ProjectPermissionSub.DynamicSecrets + | (ForcedSubject & DynamicSecretSubjectFields) + ) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & SecretImportSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Member] @@ -120,7 +142,9 @@ const CASL_ACTION_SCHEMA_NATIVE_ENUM = (actions: ACTI const CASL_ACTION_SCHEMA_ENUM = (actions: ACTION) => z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el)); -const SecretConditionSchema = z +// akhilmhdh: don't modify this for v2 +// if you want to update create a new schema +const SecretConditionV1Schema = z .object({ environment: z.union([ z.string(), @@ -146,16 +170,50 @@ const SecretConditionSchema = z }) .partial(); -export const ProjectPermissionSchema = z.discriminatedUnion("subject", [ - z.object({ - subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( - "Describe what action an entity can take." - ), - conditions: SecretConditionSchema.describe( - "When specified, only matching conditions will be allowed to access given resource." - ).optional() - }), +const SecretConditionV2Schema = z + .object({ + environment: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + secretPath: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + secretName: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + secretTags: z + .object({ + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + }) + .partial(); + +const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -259,7 +317,7 @@ export const ProjectPermissionSchema = z.discriminatedUnion("subject", [ ) }), z.object({ - subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "), + subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ) @@ -288,18 +346,78 @@ export const ProjectPermissionSchema = z.discriminatedUnion("subject", [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe( - "Describe what action an entity can take." - ) - }), z.object({ subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe( "Describe what action an entity can take." ) }) +]; + +export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ + z.object({ + subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ), + conditions: SecretConditionV1Schema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe( + "Describe what action an entity can take." + ) + }), + ...GeneralPermissionSchema +]); + +export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ + z.object({ + subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ), + conditions: SecretConditionV2Schema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ), + conditions: SecretConditionV1Schema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.SecretImports).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ), + conditions: SecretConditionV1Schema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.DynamicSecrets).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe( + "Describe what action an entity can take." + ), + conditions: SecretConditionV1Schema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + ...GeneralPermissionSchema ]); const buildAdminPermissionRules = () => { @@ -308,6 +426,8 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ ProjectPermissionSub.Secrets, + ProjectPermissionSub.SecretFolders, + ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, ProjectPermissionSub.SecretRotation, ProjectPermissionSub.Member, @@ -339,6 +459,17 @@ const buildAdminPermissionRules = () => { ); }); + can( + [ + ProjectPermissionDynamicSecretActions.ReadRootCredential, + ProjectPermissionDynamicSecretActions.EditRootCredential, + ProjectPermissionDynamicSecretActions.CreateRootCredential, + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + ProjectPermissionDynamicSecretActions.Lease + ], + ProjectPermissionSub.DynamicSecrets + ); + can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); @@ -370,6 +501,34 @@ const buildMemberPermissionRules = () => { ], ProjectPermissionSub.Secrets ); + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.SecretFolders + ); + can( + [ + ProjectPermissionDynamicSecretActions.ReadRootCredential, + ProjectPermissionDynamicSecretActions.EditRootCredential, + ProjectPermissionDynamicSecretActions.CreateRootCredential, + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + ProjectPermissionDynamicSecretActions.Lease + ], + ProjectPermissionSub.DynamicSecrets + ); + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.SecretImports + ); can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval); can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation); @@ -493,6 +652,9 @@ const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); + can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); @@ -595,17 +757,52 @@ export const isAtLeastAsPrivilegedWorkspace = ( }; /* eslint-enable */ -export const SecretV2SubjectFieldMapper = (arg: string) => { - switch (arg) { - case "environment": - return null; - case "secretPath": - return null; - case "secretName": - return `${TableName.SecretV2}.key`; - case "secretTags": - return `${TableName.SecretTag}.slug`; - default: - throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` }); - } +export const backfillPermissionV1SchemaToV2Schema = (data: z.infer[]) => { + const formattedData = UnpackedPermissionSchema.array().parse(data); + const secretSubjects = formattedData.filter((el) => el.subject === ProjectPermissionSub.Secrets); + + // this means the folder permission as readonly is set + const hasReadOnlyFolder = formattedData.filter((el) => el.subject === ProjectPermissionSub.SecretFolders); + const secretImportPolicies = secretSubjects.map(({ subject, ...el }) => ({ + ...el, + subject: ProjectPermissionSub.SecretImports as const + })); + + const secretFolderPolicies = secretSubjects.map(({ subject, ...el }) => ({ + ...el, + subject: ProjectPermissionSub.SecretFolders + })); + + const dynamicSecretPolicies = secretSubjects.map(({ subject, ...el }) => { + const action = el.action.map((e) => { + switch (e) { + case ProjectPermissionActions.Edit: + return ProjectPermissionDynamicSecretActions.EditRootCredential; + case ProjectPermissionActions.Create: + return ProjectPermissionDynamicSecretActions.CreateRootCredential; + case ProjectPermissionActions.Delete: + return ProjectPermissionDynamicSecretActions.DeleteRootCredential; + case ProjectPermissionActions.Read: + return ProjectPermissionDynamicSecretActions.ReadRootCredential; + default: + return ProjectPermissionDynamicSecretActions.ReadRootCredential; + } + }); + + return { + ...el, + action: el.action.includes(ProjectPermissionActions.Edit) + ? [...action, ProjectPermissionDynamicSecretActions.Lease] + : action, + subject: ProjectPermissionSub.DynamicSecrets + }; + }); + + return formattedData.concat( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + secretImportPolicies, + dynamicSecretPolicies, + hasReadOnlyFolder.length ? [] : secretFolderPolicies + ); }; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 1ee27b1a7..8b54c428c 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -1,11 +1,13 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, unpackRules } from "@casl/ability/extra"; import ms from "ms"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { ProjectUserAdditionalPrivilegeTemporaryMode, @@ -26,6 +28,11 @@ export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType< typeof projectUserAdditionalPrivilegeServiceFactory >; +const unpackPermissions = (permissions: unknown) => + UnpackedPermissionSchema.array().parse( + unpackRules((permissions || []) as PackRule>>[]) + ); + export const projectUserAdditionalPrivilegeServiceFactory = ({ projectUserAdditionalPrivilegeDAL, projectMembershipDAL, @@ -68,7 +75,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ slug, permissions: customPermission }); - return additionalPrivilege; + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; } const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); @@ -83,7 +93,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); - return additionalPrivilege; + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; }; const updateById = async ({ @@ -136,7 +149,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); - return additionalPrivilege; + + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; } const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { @@ -147,7 +164,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryRange: null, temporaryMode: null }); - return additionalPrivilege; + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; }; const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { @@ -174,7 +194,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); - return deletedPrivilege; + return { + ...deletedPrivilege, + permissions: unpackPermissions(deletedPrivilege.permissions) + }; }; const getPrivilegeDetailsById = async ({ @@ -206,7 +229,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); - return userPrivilege; + return { + ...userPrivilege, + permissions: unpackPermissions(userPrivilege.permissions) + }; }; const listPrivileges = async ({ @@ -233,7 +259,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ userId: projectMembership.userId, projectId: projectMembership.projectId }); - return userPrivileges; + return userPrivileges.map((el) => ({ + ...el, + permissions: unpackPermissions(el.permissions) + })); }; return { diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index e3526b0e3..bb77660aa 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -14,7 +14,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { const secretApprovalPolicyFindQuery = ( tx: Knex, - filter: TFindFilter, + filter: TFindFilter, customFilter?: { sapId?: string; } diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index e43a0d801..cb3452685 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import picomatch from "picomatch"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -356,17 +356,8 @@ export const secretApprovalPolicyServiceFactory = ({ environment, secretPath }: TGetBoardSapDTO) => { - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { secretPath, environment }) - ); + await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + return getSecretApprovalPolicy(projectId, environment, secretPath); }; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index d38043277..9db8e5d4b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -43,7 +43,7 @@ import { fnSecretBulkDelete as fnSecretV2BridgeBulkDelete, fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, - getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge + getAllSecretReferences as getAllSecretReferencesV2Bridge } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; @@ -531,11 +531,11 @@ export const secretApprovalRequestServiceFactory = ({ skipMultilineEncoding: el.skipMultilineEncoding, key: el.key, references: el.encryptedValue - ? getAllNestedSecretReferencesV2Bridge( + ? getAllSecretReferencesV2Bridge( secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - ) + ).nestedReferences : [], type: SecretType.Shared })), @@ -555,11 +555,11 @@ export const secretApprovalRequestServiceFactory = ({ ? { encryptedValue: el.encryptedValue as Buffer, references: el.encryptedValue - ? getAllNestedSecretReferencesV2Bridge( + ? getAllSecretReferencesV2Bridge( secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - ) + ).nestedReferences : [] } : {}; @@ -1143,10 +1143,6 @@ export const secretApprovalRequestServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1309,7 +1305,24 @@ export const secretApprovalRequestServiceFactory = ({ const tagIds = unique(Object.values(commitTagIds).flat()); const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : []; - if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); + if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" }); + const tagsGroupById = groupBy(tags, (i) => i.id); + + commits.forEach((commit) => { + let action = ProjectPermissionActions.Create; + if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; + if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; + + ForbiddenError.from(permission).throwUnlessCan( + action, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: commit.key, + secretTags: commitTagIds?.[commit.key]?.map((secretTagId) => tagsGroupById[secretTagId][0].slug) + }) + ); + }); const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => { const doc = await secretApprovalRequestDAL.create( diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index cdb36f7af..81d467bab 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -28,8 +28,7 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, - getAllNestedSecretReferences, - getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge + getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; @@ -253,11 +252,12 @@ export const secretReplicationServiceFactory = ({ const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared }); const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id }); const sourceImportedSecrets = await fnSecretsV2FromImports({ - allowedImports: sourceSecretImports, + secretImports: sourceSecretImports, secretDAL: secretV2BridgeDAL, folderDAL, secretImportDAL, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), + hasSecretAccess: () => true }); // secrets that gets replicated across imports const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({ @@ -419,7 +419,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, - references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : [] + references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] }; }) }); @@ -445,7 +445,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue as Buffer, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, - references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : [] + references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] } }; }) @@ -694,7 +694,7 @@ export const secretReplicationServiceFactory = ({ secretCommentTag: doc.secretCommentTag, secretCommentCiphertext: doc.secretCommentCiphertext, skipMultilineEncoding: doc.skipMultilineEncoding, - references: getAllNestedSecretReferences(doc.secretValue) + references: getAllSecretReferences(doc.secretValue).nestedReferences }; }) }); @@ -730,7 +730,7 @@ export const secretReplicationServiceFactory = ({ secretCommentTag: doc.secretCommentTag, secretCommentCiphertext: doc.secretCommentCiphertext, skipMultilineEncoding: doc.skipMultilineEncoding, - references: getAllNestedSecretReferences(doc.secretValue) + references: getAllSecretReferences(doc.secretValue).nestedReferences } }; }) diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index ec4171f2b..6dde2657f 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import Ajv from "ajv"; -import { ProjectVersion } from "@app/db/schemas"; +import { ProjectVersion, TableName } from "@app/db/schemas"; import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -103,13 +103,14 @@ export const secretRotationServiceFactory = ({ ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); + const project = await projectDAL.findById(projectId); const shouldUseBridge = project.version === ProjectVersion.V3; if (shouldUseBridge) { const selectedSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, - $in: { id: Object.values(outputs) } + $in: { [`${TableName.SecretV2}.id` as "id"]: Object.values(outputs) } }); if (selectedSecrets.length !== Object.values(outputs).length) throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` }); diff --git a/backend/src/lib/casl/knex.ts b/backend/src/lib/casl/knex.ts deleted file mode 100644 index cc9718fe0..000000000 --- a/backend/src/lib/casl/knex.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { AnyAbility, ExtractSubjectType } from "@casl/ability"; -import { AbilityQuery, rulesToQuery } from "@casl/ability/extra"; -import { Tables } from "knex/types/tables"; - -import { BadRequestError, UnauthorizedError } from "../errors"; -import { TKnexDynamicOperator } from "../knex/dynamic"; - -type TBuildKnexQueryFromCaslDTO = { - ability: K; - subject: ExtractSubjectType[1]>; - action: Parameters[0]; -}; - -export const buildKnexQueryFromCaslOperators = ({ - ability, - subject, - action -}: TBuildKnexQueryFromCaslDTO) => { - const query = rulesToQuery(ability, action, subject, (rule) => { - if (!rule.ast) throw new Error("Ast not defined"); - return rule.ast; - }); - - if (query === null) throw new UnauthorizedError({ message: `You don't have permission to do ${action} ${subject}` }); - return query; -}; - -type TFieldMapper = { - [K in T]: `${K}.${Exclude}`; -}[T]; - -type TFormatCaslFieldsWithTableNames = { - // handle if any missing operator else throw error let the app break because this is executing again the db - missingOperatorCallback?: (operator: string) => void; - fieldMapping: (arg: string) => TFieldMapper | null; - dynamicQuery: TKnexDynamicOperator; -}; - -export const formatCaslOperatorFieldsWithTableNames = ({ - missingOperatorCallback = (arg) => { - throw new BadRequestError({ message: `Unknown permission operator: ${arg}` }); - }, - dynamicQuery: dynamicQueryAst, - fieldMapping -}: TFormatCaslFieldsWithTableNames) => { - const stack: [TKnexDynamicOperator, TKnexDynamicOperator | null][] = [[dynamicQueryAst, null]]; - - while (stack.length) { - const [filterAst, parentAst] = stack.pop()!; - - if (filterAst.operator === "and" || filterAst.operator === "or" || filterAst.operator === "not") { - filterAst.value.forEach((el) => { - stack.push([el, filterAst]); - }); - - // eslint-disable-next-line no-continue - continue; - } - - if ( - filterAst.operator === "eq" || - filterAst.operator === "ne" || - filterAst.operator === "in" || - filterAst.operator === "endsWith" || - filterAst.operator === "startsWith" - ) { - const attrPath = fieldMapping(filterAst.field); - if (attrPath) { - filterAst.field = attrPath; - } else if (parentAst && Array.isArray(parentAst.value)) { - parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[]; - } else throw new Error("Unknown casl field"); - // eslint-disable-next-line no-continue - continue; - } - - if (parentAst && Array.isArray(parentAst.value)) { - parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[]; - } else { - missingOperatorCallback?.(filterAst.operator); - } - } - return dynamicQueryAst; -}; - -export const convertCaslOperatorToKnexOperator = ( - caslKnexOperators: AbilityQuery, - fieldMapping: (arg: string) => TFieldMapper | null -) => { - const value = []; - if (caslKnexOperators.$and) { - value.push({ - operator: "not" as const, - value: caslKnexOperators.$and as TKnexDynamicOperator[] - }); - } - if (caslKnexOperators.$or) { - value.push({ - operator: "or" as const, - value: caslKnexOperators.$or as TKnexDynamicOperator[] - }); - } - - return formatCaslOperatorFieldsWithTableNames({ - dynamicQuery: { - operator: "and", - value - }, - fieldMapping - }); -}; diff --git a/backend/src/lib/fn/array.ts b/backend/src/lib/fn/array.ts index e7db061f3..760317bad 100644 --- a/backend/src/lib/fn/array.ts +++ b/backend/src/lib/fn/array.ts @@ -81,3 +81,25 @@ export const chunkArray = (array: T[], chunkSize: number): T[][] => { } return chunks; }; + +/* + * Returns all items from the first list that + * do not exist in the second list. + */ +export const diff = ( + root: readonly T[], + other: readonly T[], + identity: (item: T) => string | number | symbol = (t: T) => t as unknown as string | number | symbol +): T[] => { + if (!root?.length && !other?.length) return []; + if (root?.length === undefined) return [...other]; + if (!other?.length) return [...root]; + const bKeys = other.reduce( + (acc, item) => { + acc[identity(item)] = true; + return acc; + }, + {} as Record + ); + return root.filter((a) => !bKeys[identity(a)]); +}; diff --git a/backend/src/lib/knex/dynamic.ts b/backend/src/lib/knex/dynamic.ts index c336d7a9e..b8bc8ab57 100644 --- a/backend/src/lib/knex/dynamic.ts +++ b/backend/src/lib/knex/dynamic.ts @@ -2,32 +2,31 @@ import { Knex } from "knex"; import { UnauthorizedError } from "../errors"; -type TKnexDynamicPrimitiveOperator = { +type TKnexDynamicPrimitiveOperator = { operator: "eq" | "ne" | "startsWith" | "endsWith"; value: string; - field: string; + field: Extract; }; -type TKnexDynamicInOperator = { +type TKnexDynamicInOperator = { operator: "in"; value: string[] | number[]; - field: string; + field: Extract; }; -type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator; +type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator; -type TKnexGroupOperator = { +type TKnexGroupOperator = { operator: "and" | "or" | "not"; - value: (TKnexNonGroupOperator | TKnexGroupOperator)[]; + value: (TKnexNonGroupOperator | TKnexGroupOperator)[]; }; -// akhilmhdh: This is still in pending state and not yet ready. If you want to use it ping me. -// used when you need to write a complex query with the orm -// use it when you need complex or and and condition - most of the time not needed -// majorly used with casl permission to filter data based on permission -export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator; +export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator; -export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => { +export const buildDynamicKnexQuery = ( + rootQueryBuild: Knex.QueryBuilder, + dynamicQuery: TKnexDynamicOperator +) => { const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }]; while (stack.length) { @@ -50,34 +49,25 @@ export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQu break; } case "and": { - void queryBuilder.andWhere((subQueryBuilder) => { - filterAst.value.forEach((el) => { - stack.push({ - queryBuilder: subQueryBuilder, - filterAst: el - }); + filterAst.value.forEach((el) => { + void queryBuilder.andWhere((subQueryBuilder) => { + buildDynamicKnexQuery(subQueryBuilder, el); }); }); break; } case "or": { - void queryBuilder.orWhere((subQueryBuilder) => { - filterAst.value.forEach((el) => { - stack.push({ - queryBuilder: subQueryBuilder, - filterAst: el - }); + filterAst.value.forEach((el) => { + void queryBuilder.orWhere((subQueryBuilder) => { + buildDynamicKnexQuery(subQueryBuilder, el); }); }); break; } case "not": { - void queryBuilder.whereNot((subQueryBuilder) => { - filterAst.value.forEach((el) => { - stack.push({ - queryBuilder: subQueryBuilder, - filterAst: el - }); + filterAst.value.forEach((el) => { + void queryBuilder.whereNot((subQueryBuilder) => { + buildDynamicKnexQuery(subQueryBuilder, el); }); }); break; diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 36d81ae34..f55d8e6e6 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -3,6 +3,7 @@ import { Knex } from "knex"; import { Tables } from "knex/types/tables"; import { DatabaseError } from "../errors"; +import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic"; export * from "./connection"; export * from "./join"; @@ -20,9 +21,10 @@ export const withTransaction = (db: Knex, dal: K) => ({ export type TFindFilter = Partial & { $in?: Partial<{ [k in keyof R]: R[k][] }>; $search?: Partial<{ [k in keyof R]: R[k] }>; + $complex?: TKnexDynamicOperator; }; export const buildFindFilter = - ({ $in, $search, ...filter }: TFindFilter) => + ({ $in, $search, $complex, ...filter }: TFindFilter) => (bd: Knex.QueryBuilder) => { void bd.where(filter); if ($in) { @@ -39,6 +41,9 @@ export const buildFindFilter = } }); } + if ($complex) { + return buildDynamicKnexQuery(bd, $complex); + } return bd; }; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index be8665a84..007902a17 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -63,7 +63,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider void res.status(HttpStatusCodes.Forbidden).send({ statusCode: HttpStatusCodes.Forbidden, error: "PermissionDenied", - message: `You are not allowed to ${error.action} on ${error.subjectType}` + message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}` }); } else if (error instanceof ForbiddenRequestError) { void res.status(HttpStatusCodes.Forbidden).send({ diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 68df7f2d9..827008ca4 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router"; import { registerV1EERoutes } from "@app/ee/routes/v1"; +import { registerV2EERoutes } from "@app/ee/routes/v2"; import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; @@ -1422,7 +1423,13 @@ export const registerRoutes = async ( }, { prefix: "/api/v1" } ); - await server.register(registerV2Routes, { prefix: "/api/v2" }); + await server.register( + async (v2Server) => { + await v2Server.register(registerV2EERoutes); + await v2Server.register(registerV2Routes); + }, + { prefix: "/api/v2" } + ); await server.register(registerV3Routes, { prefix: "/api/v3" }); server.addHook("onClose", async () => { diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index aa53ed50e..2bf170fc0 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -9,9 +9,10 @@ import { SecretApprovalPoliciesSchema, UsersSchema } from "@app/db/schemas"; -import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { UnpackedPermissionSchema } from "./santizedSchemas/permission"; + // sometimes the return data must be santizied to avoid leaking important values // always prefer pick over omit in zod export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({ diff --git a/backend/src/server/routes/santizedSchemas/permission.ts b/backend/src/server/routes/santizedSchemas/permission.ts new file mode 100644 index 000000000..94c1dc57c --- /dev/null +++ b/backend/src/server/routes/santizedSchemas/permission.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const UnpackedPermissionSchema = z.object({ + subject: z + .union([z.string().min(1), z.string().array()]) + .transform((el) => (typeof el !== "string" ? el[0] : el)) + .optional(), + action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), + conditions: z.unknown().optional(), + inverted: z.boolean().optional() +}); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index f8e02365a..36f0df591 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -3,7 +3,10 @@ import { z } from "zod"; import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { DASHBOARD } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; @@ -192,15 +195,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { req.permission.orgId ); - const permissiveEnvs = // filter envs user has access to + const allowedDynamicSecretEnvironments = // filter envs user has access to environments.filter((environment) => permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) ) ); - if (includeDynamicSecrets && permissiveEnvs.length) { + if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) { // this is the unique count, ie duplicate secrets across envs only count as 1 totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({ actor: req.permission.type, @@ -209,7 +212,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, projectId, search, - environmentSlugs: permissiveEnvs, + environmentSlugs: allowedDynamicSecretEnvironments, path: secretPath, isInternal: true }); @@ -224,7 +227,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, orderBy, orderDirection, - environmentSlugs: permissiveEnvs, + environmentSlugs: allowedDynamicSecretEnvironments, path: secretPath, limit: remainingLimit, offset: adjustedOffset, @@ -241,13 +244,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } - if (includeSecrets && permissiveEnvs.length) { + if (includeSecrets) { // this is the unique count, ie duplicate secrets across envs only count as 1 totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({ actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, - environments: permissiveEnvs, + environments, actorAuthMethod: req.permission.authMethod, projectId, path: secretPath, @@ -260,7 +263,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, - environments: permissiveEnvs, + environments, actorAuthMethod: req.permission.authMethod, projectId, path: secretPath, @@ -272,7 +275,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { isInternal: true }); - for await (const environment of permissiveEnvs) { + for await (const environment of environments) { const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length; if (secretCountFromEnv) { diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index 6d996022a..934c1c8e1 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -19,7 +19,7 @@ import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; -import { fnSecretBulkInsert, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; +import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; @@ -242,7 +242,7 @@ export const importDataIntoInfisicalFn = async ({ } await fnSecretBulkInsert({ inputSecrets: secretBatch.map((el) => { - const references = getAllNestedSecretReferences(el.secretValue); + const references = getAllSecretReferences(el.secretValue).nestedReferences; return { version: 1, diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index 8036ac7b9..fdefd0e62 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -67,7 +67,8 @@ const getIntegrationSecretsV2 = async ( folderDAL, secretDAL: secretV2BridgeDAL, secretImportDAL, - allowedImports: secretImports + secretImports, + hasSecretAccess: () => true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 9b5ff0cf9..12f4c77de 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -90,7 +90,10 @@ export const integrationServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath + }) ); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath); @@ -167,7 +170,10 @@ export const integrationServiceFactory = ({ if (environment || secretPath) { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: newEnvironment, + secretPath: newSecretPath + }) ); } diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 1fda78e32..e839752c5 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -2,7 +2,6 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { ProjectMembershipRole } from "@app/db/schemas"; -import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -10,6 +9,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { ActorAuthMethod } from "../auth/auth-type"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts deleted file mode 100644 index c8f7d885e..000000000 --- a/backend/src/services/secret-folder/secret-folder-fns.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RawRule } from "@casl/ability"; - -import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; - -export const shouldCheckFolderPermission = (rules: RawRule[]) => - rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)); diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 8eeef1a22..91d64b5a2 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -12,7 +12,6 @@ import { OrderByDirection } from "@app/lib/types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretFolderDALFactory } from "./secret-folder-dal"; -import { shouldCheckFolderPermission } from "./secret-folder-fns"; import { TCreateFolderDTO, TDeleteFolderDTO, @@ -60,20 +59,10 @@ export const secretFolderServiceFactory = ({ actorOrgId ); - // we do this because we've split Secret and SecretFolder resources - // previously, if one can create/update/read/delete secrets then they can do the same for folders - // for backwards compatibility, we handle authorization only when SecretFolders subject is used - if (shouldCheckFolderPermission(permission.rules)) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) - ); - } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) { @@ -169,20 +158,10 @@ export const secretFolderServiceFactory = ({ ); folders.forEach(({ environment, path: secretPath }) => { - // we do this because we've split Secret and SecretFolder resources - // previously, if one can create/update/read/delete secrets then they can do the same for folders - // for backwards compatibility, we handle authorization only when SecretFolders subject is used - if (shouldCheckFolderPermission(permission.rules)) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) - ); - } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); }); const result = await folderDAL.transaction(async (tx) => @@ -287,20 +266,10 @@ export const secretFolderServiceFactory = ({ actorOrgId ); - // we do this because we've split Secret and SecretFolder resources - // previously, if one can create/update/read/delete secrets then they can do the same for folders - // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used - if (shouldCheckFolderPermission(permission.rules)) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) - ); - } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!parentFolder) @@ -377,20 +346,10 @@ export const secretFolderServiceFactory = ({ actorOrgId ); - // we do this because we've split Secret and SecretFolder resources - // previously, if one can create/update/read/delete secrets then they can do the same for folders - // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used - if (shouldCheckFolderPermission(permission.rules)) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) - ); - } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index b21a6c3ce..d75a25514 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -27,6 +27,7 @@ type TSecretImportSecretsV2 = { slug: string; name: string; }; + id: string; folderId: string | undefined; importFolderId: string; secrets: (TSecretsV2 & { @@ -139,24 +140,22 @@ export const fnSecretsFromImports = async ({ return secrets; }; +/* eslint-disable no-await-in-loop, no-continue */ export const fnSecretsV2FromImports = async ({ - allowedImports: possibleCyclicImports, + secretImports: rootSecretImports, folderDAL, secretDAL, secretImportDAL, - depth = 0, - cyclicDetector = new Set(), decryptor, - expandSecretReferences + expandSecretReferences, + hasSecretAccess }: { - allowedImports: (Omit & { + secretImports: (Omit & { importEnv: { id: string; slug: string; name: string }; })[]; folderDAL: Pick; secretDAL: Pick; secretImportDAL: Pick; - depth?: number; - cyclicDetector?: Set; decryptor: (value?: Buffer | null) => string; expandSecretReferences?: (inputSecret: { value?: string; @@ -164,92 +163,107 @@ export const fnSecretsV2FromImports = async ({ secretPath: string; environment: string; }) => Promise; + hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { - // avoid going more than a depth - if (depth >= LEVEL_BREAK) return []; + const cyclicDetector = new Set(); + const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ + { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } + ]; - const allowedImports = possibleCyclicImports.filter( - ({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath)) - ); + const processedImports: TSecretImportSecretsV2[] = []; - const importedFolders = ( - await folderDAL.findByManySecretPath( - allowedImports.map(({ importEnv, importPath }) => ({ + while (stack.length) { + const { secretImports, depth, parentImportedSecrets } = stack.pop()!; + + if (depth > LEVEL_BREAK) continue; + const sanitizedImports = secretImports.filter( + ({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath)) + ); + + if (!sanitizedImports.length) continue; + + const importedFolders = await folderDAL.findByManySecretPath( + sanitizedImports.map(({ importEnv, importPath }) => ({ envId: importEnv.id, secretPath: importPath })) - ) - ).filter(Boolean); // remove undefined ones - if (!importedFolders.length) { - return []; - } + ); + if (!importedFolders.length) continue; - const importedFolderIds = importedFolders.map((el) => el?.id) as string[]; - const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`); - const importedSecrets = await secretDAL.find( - { - $in: { folderId: importedFolderIds }, - type: SecretType.Shared - }, - { - sort: [["id", "asc"]] - } - ); + const importedFolderIds = importedFolders.map((el) => el?.id) as string[]; + const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`); - const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); + const importedSecrets = await secretDAL.find( + { + $in: { folderId: importedFolderIds }, + type: SecretType.Shared + }, + { + sort: [["id", "asc"]] + } + ); + const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); - allowedImports.forEach(({ importPath, importEnv }) => { - cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath)); - }); - // now we need to check recursively deeper imports made inside other imports - // we go level wise meaning we take all imports of a tree level and then go deeper ones level by level - const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds); - let secretsFromDeeperImports: TSecretImportSecretsV2[] = []; - if (deeperImports.length) { - secretsFromDeeperImports = await fnSecretsV2FromImports({ - allowedImports: deeperImports.filter(({ isReplication }) => !isReplication), - secretImportDAL, - folderDAL, - secretDAL, - depth: depth + 1, - cyclicDetector, - decryptor, - expandSecretReferences + sanitizedImports.forEach(({ importPath, importEnv }) => { + cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath)); + }); + // now we need to check recursively deeper imports made inside other imports + // we go level wise meaning we take all imports of a tree level and then go deeper ones level by level + const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds); + const deeperImportsGroupByFolderId = groupBy(deeperImports, (i) => i.folderId); + + const isFirstIteration = !processedImports.length; + sanitizedImports.forEach(({ importPath, importEnv, id, folderId }, i) => { + const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0]; + const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []) + .filter((item) => + hasSecretAccess( + importEnv.slug, + importPath, + item.key, + item.tags.map((el) => el.slug) + ) + ) + .map((item) => ({ + ...item, + secretKey: item.key, + secretValue: decryptor(item.encryptedValue), + secretComment: decryptor(item.encryptedComment), + environment: importEnv.slug, + workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. + _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. + })); + + if (deeperImportsGroupByFolderId?.[sourceImportFolder?.id || ""]) { + stack.push({ + secretImports: deeperImportsGroupByFolderId[sourceImportFolder?.id || ""], + depth: depth + 1, + parentImportedSecrets: secretsWithDuplicate + }); + } + + if (isFirstIteration) { + processedImports.push({ + secretPath: importPath, + environment: importEnv.slug, + environmentInfo: importEnv, + folderId: importedFolders?.[i]?.id, + id, + importFolderId: folderId, + secrets: secretsWithDuplicate + }); + } else { + parentImportedSecrets.push(...secretsWithDuplicate); + } }); } - const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId); - - const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => { - const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0]; - const folderDeeperImportSecrets = - secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || []; - const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []) - .map((item) => ({ - ...item, - secretKey: item.key, - secretValue: decryptor(item.encryptedValue), - secretComment: decryptor(item.encryptedComment), - environment: importEnv.slug, - workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. - _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. - })) - .concat(folderDeeperImportSecrets); - - return { - secretPath: importPath, - environment: importEnv.slug, - environmentInfo: importEnv, - folderId: importedFolders?.[i]?.id, - id, - importFolderId: folderId, - secrets: unique(secretsWithDuplicate, (el) => el.secretKey) - }; - }); - + /* eslint-enable */ if (expandSecretReferences) { await Promise.allSettled( - processedImports.map((processedImport) => - Promise.allSettled( + processedImports.map((processedImport) => { + // eslint-disable-next-line + processedImport.secrets = unique(processedImport.secrets, (i) => i.key); + return Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, @@ -260,8 +274,8 @@ export const fnSecretsV2FromImports = async ({ // eslint-disable-next-line no-param-reassign processedImport.secrets[index].secretValue = expandedSecretValue || ""; }) - ) - ) + ); + }) ); } diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index fad32a67a..25e78fb65 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -84,12 +84,12 @@ export const secretImportServiceFactory = ({ // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -198,7 +198,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -292,7 +292,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -364,8 +364,8 @@ export const secretImportServiceFactory = ({ // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -393,7 +393,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -441,7 +441,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -476,7 +476,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -526,7 +526,7 @@ export const secretImportServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { + subject(ProjectPermissionSub.SecretImports, { environment: folder.environment.envSlug, secretPath: folderWithPath.path }) @@ -573,20 +573,19 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) return []; // this will already order by position // so anything based on this order will also be in right position const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); - - const allowedImports = secretImports.filter(({ importEnv, importPath }) => + const allowedImports = secretImports.filter((el) => permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath + environment: el.importEnv.slug, + secretPath: el.importPath }) ) ); @@ -611,7 +610,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) return []; @@ -619,16 +618,6 @@ export const secretImportServiceFactory = ({ // so anything based on this order will also be in right position const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); - const allowedImports = secretImports.filter(({ importEnv, importPath }) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) - ); - const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); if (shouldUseSecretV2Bridge) { const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ @@ -636,11 +625,21 @@ export const secretImportServiceFactory = ({ projectId }); const importedSecrets = await fnSecretsV2FromImports({ - allowedImports, + secretImports, folderDAL, secretDAL: secretV2BridgeDAL, secretImportDAL, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ) }); return importedSecrets; } @@ -651,7 +650,21 @@ export const secretImportServiceFactory = ({ name: "bot_not_found_error" }); - const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); + const allowedImports = secretImports.filter((el) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: el.importEnv.slug, + secretPath: el.importPath + }) + ) + ); + const importedSecrets = await fnSecretsFromImports({ + allowedImports, + folderDAL, + secretDAL, + secretImportDAL + }); return importedSecrets.map((el) => ({ ...el, secrets: el.secrets.map((encryptedSecret) => diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 1a9397e91..adbd935ee 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -4,7 +4,14 @@ import { validate as uuidValidate } from "uuid"; import { TDbClient } from "@app/db"; import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { + buildFindFilter, + ormify, + selectAllTableCols, + sqlNestRelationships, + TFindFilter, + TFindOpt +} from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -13,6 +20,97 @@ export type TSecretV2BridgeDALFactory = ReturnType { const secretOrm = ormify(db, TableName.SecretV2); + const findOne = async (filter: Partial, tx?: Knex) => { + try { + const docs = await (tx || db)(TableName.SecretV2) + .where(filter) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretV2}: FindOne` }); + } + }; + + const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db)(TableName.SecretV2) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(filter)) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + 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 = await query; + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` }); + } + }; + const update = async (filter: Partial, data: Omit, tx?: Knex) => { try { const sec = await (tx || db)(TableName.SecretV2) @@ -484,6 +582,8 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - countByFolderIds + countByFolderIds, + findOne, + find }; }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 32fc55285..ab946a2af 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -30,9 +30,10 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3; * // { environment: 'prod', secretPath: '/anotherFolder' } * // ] */ -export const getAllNestedSecretReferences = (maybeSecretReference: string) => { +export const getAllSecretReferences = (maybeSecretReference: string) => { const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]); - return references + + const nestedReferences = references .filter((el) => el.includes(".")) .map((el) => { const [environment, ...secretPathList] = el.split("."); @@ -42,6 +43,8 @@ export const getAllNestedSecretReferences = (maybeSecretReference: string) => { secretKey: secretPathList[secretPathList.length - 1] }; }); + const localReferences = references.filter((el) => !el.includes(".")); + return { nestedReferences, localReferences }; }; // these functions are special functions shared by a couple of resources @@ -325,16 +328,13 @@ type TRecursivelyFetchSecretsFromFoldersArg = { projectId: string; environment: string; currentPath: string; - hasAccess: (environment: string, secretPath: string) => boolean; }; export const recursivelyGetSecretPaths = async ({ folderDAL, projectEnvDAL, projectId, - environment, - currentPath, - hasAccess + environment }: TRecursivelyFetchSecretsFromFoldersArg) => { const env = await projectEnvDAL.findOne({ projectId, @@ -362,12 +362,7 @@ export const recursivelyGetSecretPaths = async ({ folderId: p.folderId })); - // Filter out paths that the user does not have permission to access, and paths that are not in the current path - const allowedPaths = paths.filter( - (folder) => hasAccess(environment, folder.path) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) - ); - - return allowedPaths; + return paths; }; // used to convert multi line ones to quotes ones with \n const formatMultiValueEnv = (val?: string) => { @@ -381,7 +376,7 @@ type TInterpolateSecretArg = { decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined; secretDAL: Pick; folderDAL: Pick; - canExpandValue: (environment: string, secretPath: string) => boolean; + canExpandValue: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }; const MAX_SECRET_REFERENCE_DEPTH = 10; @@ -392,29 +387,29 @@ export const expandSecretReferencesFactory = ({ folderDAL, canExpandValue }: TInterpolateSecretArg) => { - const secretCache: Record> = {}; + const secretCache: Record> = {}; const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => { const cacheKey = getCacheUniqueKey(environment, secretPath); if (secretCache?.[cacheKey]) { - return secretCache[cacheKey][secretKey] || ""; + return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; } const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) return ""; + if (!folder) return { value: "", tags: [] }; const secrets = await secretDAL.findByFolderId(folder.id); - const decryptedSecret = secrets.reduce>((prev, secret) => { + const decryptedSecret = secrets.reduce>((prev, secret) => { // eslint-disable-next-line no-param-reassign - prev[secret.key] = decryptSecret(secret.encryptedValue) || ""; + prev[secret.key] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) }; return prev; }, {}); secretCache[cacheKey] = decryptedSecret; - return secretCache[cacheKey][secretKey] || ""; + return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; }; const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => { @@ -440,43 +435,43 @@ export const expandSecretReferencesFactory = ({ if (entities.length === 1) { const [secretKey] = entities; - if (!canExpandValue(environment, secretPath)) + // eslint-disable-next-line no-continue,no-await-in-loop + const referredValue = await fetchSecret(environment, secretPath, secretKey); + if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) throw new ForbiddenRequestError({ message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` }); - // eslint-disable-next-line no-continue,no-await-in-loop - const referedValue = await fetchSecret(environment, secretPath, secretKey); const cacheKey = getCacheUniqueKey(environment, secretPath); - secretCache[cacheKey][secretKey] = referedValue; - if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { + secretCache[cacheKey][secretKey] = referredValue; + if (INTERPOLATION_SYNTAX_REG.test(referredValue.value)) { stack.push({ - value: referedValue, + value: referredValue.value, secretPath, environment, depth: depth + 1 }); } - if (referedValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + if (referredValue) { + expandedValue = expandedValue.replaceAll(interpolationSyntax, referredValue.value); } } else { const secretReferenceEnvironment = entities[0]; const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); const secretReferenceKey = entities[entities.length - 1]; - if (!canExpandValue(secretReferenceEnvironment, secretReferencePath)) + // eslint-disable-next-line no-await-in-loop + const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); + if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` }); - // eslint-disable-next-line no-await-in-loop - const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); secretCache[cacheKey][secretReferenceKey] = referedValue; - if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { + if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) { stack.push({ - value: referedValue, + value: referedValue.value, secretPath: secretReferencePath, environment: secretReferenceEnvironment, depth: depth + 1 @@ -484,7 +479,7 @@ export const expandSecretReferencesFactory = ({ } if (referedValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value); } } } diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 53c9c43ef..756e1f810 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1,6 +1,7 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError, PureAbility, subject } from "@casl/ability"; +import { z } from "zod"; -import { ProjectMembershipRole, SecretsV2Schema, SecretType } from "@app/db/schemas"; +import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -8,7 +9,7 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; +import { diff, groupBy } from "@app/lib/fn"; import { setKnexStringValue } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -28,7 +29,7 @@ import { fnSecretBulkDelete, fnSecretBulkInsert, fnSecretBulkUpdate, - getAllNestedSecretReferences, + getAllSecretReferences, recursivelyGetSecretPaths, reshapeBridgeSecret } from "./secret-v2-bridge-fns"; @@ -43,6 +44,7 @@ import { TGetSecretsDTO, TGetSecretVersionsDTO, TMoveSecretsDTO, + TSecretReference, TUpdateManySecretDTO, TUpdateSecretDTO } from "./secret-v2-bridge-types"; @@ -56,7 +58,7 @@ type TSecretV2BridgeServiceFactoryDep = { secretVersionTagDAL: Pick; secretTagDAL: TSecretTagDALFactory; permissionService: Pick; - projectEnvDAL: Pick; + projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" @@ -93,6 +95,83 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalRequestSecretDAL, kmsService }: TSecretV2BridgeServiceFactoryDep) => { + const $validateSecretReferences = async ( + projectId: string, + permission: PureAbility, + references: ReturnType["nestedReferences"] + ) => { + if (!references.length) return; + + const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment))); + const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs); + if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length) + throw new BadRequestError({ + message: `Referenced environment not found. Missing ${diff( + uniqueReferenceEnvironmentSlugs, + referencesEnvironments.map((el) => el.slug) + ).join(",")}` + }); + + const referencesEnvironmentGroupBySlug = groupBy(referencesEnvironments, (i) => i.slug); + const referredFolders = await folderDAL.findByManySecretPath( + references.map((el) => ({ + secretPath: el.secretPath, + envId: referencesEnvironmentGroupBySlug[el.environment][0].id + })) + ); + const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`); + const referredSecrets = await secretDAL.find({ + $complex: { + operator: "or", + value: references.map((el) => { + const folderId = + referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`][0] + ?.id; + if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` }); + + return { + operator: "and", + value: [ + { + operator: "eq", + field: "folderId", + value: folderId + }, + { + operator: "eq", + field: "key", + value: el.secretKey + } + ] + }; + }) + } + }); + + if (referredSecrets.length !== references.length) + throw new BadRequestError({ + message: `Referenced secret not found. Found only ${diff( + references.map((el) => el.secretKey), + referredSecrets.map((el) => el.key) + ).join(",")}` + }); + + const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); + references.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: el.environment, + secretPath: el.secretPath, + secretName: el.secretKey, + tags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) + }) + ); + }); + + return referredSecrets; + }; + const createSecret = async ({ actor, actorId, @@ -110,10 +189,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -134,6 +209,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (inputSecret.type === SecretType.Shared && doesSecretExist) throw new BadRequestError({ message: "Secret already exist" }); + // if user creating personal check its shared also exist if (inputSecret.type === SecretType.Personal && !doesSecretExist) { throw new BadRequestError({ @@ -145,24 +221,30 @@ export const secretV2BridgeServiceFactory = ({ // fetch all tags and if not same count throw error meaning one was invalid tags const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; if ((inputSecret.tagIds || []).length !== tags.length) - throw new NotFoundError({ message: "One or more tags not found" }); + throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); + + const { secretName, type, ...inputSecretData } = inputSecret; + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: tags?.map((el) => el.slug) + }) + ); + + const { nestedReferences, localReferences } = getAllSecretReferences(inputSecret.secretValue); + const allSecretReferences = nestedReferences.concat( + localReferences.map((el) => ({ secretKey: el, secretPath, environment })) + ); + await $validateSecretReferences(projectId, permission, allSecretReferences); - const { secretName, type, ...el } = inputSecret; - const references = getAllNestedSecretReferences(inputSecret.secretValue); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); - const secret = await secretDAL.transaction((tx) => fnSecretBulkInsert({ folderId, @@ -170,20 +252,20 @@ export const secretV2BridgeServiceFactory = ({ { version: 1, type, - reminderRepeatDays: el.secretReminderRepeatDays, + reminderRepeatDays: inputSecretData.secretReminderRepeatDays, encryptedComment: setKnexStringValue( - el.secretComment, + inputSecretData.secretComment, (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob ), - encryptedValue: el.secretValue - ? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob + encryptedValue: inputSecretData.secretValue + ? secretManagerEncryptor({ plainText: Buffer.from(inputSecretData.secretValue) }).cipherTextBlob : undefined, - reminderNote: el.secretReminderNote, - skipMultilineEncoding: el.skipMultilineEncoding, + reminderNote: inputSecretData.secretReminderNote, + skipMultilineEncoding: inputSecretData.skipMultilineEncoding, key: secretName, userId: inputSecret.type === SecretType.Personal ? actorId : null, tagIds: inputSecret.tagIds, - references + references: nestedReferences } ], secretDAL, @@ -229,10 +311,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); if (inputSecret.newSecretName === "") { throw new BadRequestError({ message: "New secret name cannot be empty" }); @@ -279,6 +357,33 @@ export const secretV2BridgeServiceFactory = ({ secret = sharedSecretToModify; } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + + // validate tags + // fetch all tags and if not same count throw error meaning one was invalid tags + const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; + if ((inputSecret.tagIds || []).length !== tags.length) + throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); + + // now check with new ids + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + secretTags: tags?.map((el) => el.slug) + }) + ); + if (inputSecret.newSecretName) { const doesNewNameSecretExist = await secretDAL.findOne({ key: inputSecret.newSecretName, @@ -286,37 +391,36 @@ export const secretV2BridgeServiceFactory = ({ folderId }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.newSecretName, + secretTags: tags?.map((el) => el.slug) + }) + ); } - // validate tags - // fetch all tags and if not same count throw error meaning one was invalid tags - const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; - if ((inputSecret.tagIds || []).length !== tags.length) - throw new NotFoundError({ message: "One or more tags not found" }); - const { secretName, secretValue } = inputSecret; const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const encryptedValue = - typeof secretValue !== "undefined" - ? { - encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob, - references: getAllNestedSecretReferences(secretValue) - } - : {}; - if (encryptedValue.references) { - encryptedValue.references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); + const encryptedValue = secretValue + ? { + encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob, + references: getAllSecretReferences(secretValue).nestedReferences + } + : {}; + + if (secretValue) { + const { nestedReferences, localReferences } = getAllSecretReferences(secretValue); + const allSecretReferences = nestedReferences.concat( + localReferences.map((el) => ({ secretKey: el, secretPath, environment })) + ); + await $validateSecretReferences(projectId, permission, allSecretReferences); } const updatedSecret = await secretDAL.transaction(async (tx) => @@ -390,10 +494,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -417,11 +517,16 @@ export const secretV2BridgeServiceFactory = ({ userId: actorId }) }); - if (!secretToDelete) - throw new NotFoundError({ - message: `Secret with name '${inputSecret.secretName}' not found`, - name: "DeleteSecret" - }); + if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }) + ); const deletedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkDelete({ @@ -490,13 +595,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - // verify user has access to all environments - environments.forEach((environment) => - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ) - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -542,10 +641,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -570,22 +666,15 @@ export const secretV2BridgeServiceFactory = ({ environments: string[]; isInternal?: boolean; }) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); if (!isInternal) { - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId - ); - - // verify user has access to all environments - environments.forEach((environment) => - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ) - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); } let paths: { folderId: string; path: string; environment: string }[] = []; @@ -612,22 +701,34 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - const decryptedSecrets = secrets.map((secret) => - reshapeBridgeSecret( - projectId, - groupedPaths[secret.folderId][0].environment, - groupedPaths[secret.folderId][0].path, - { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - } + const decryptedSecrets = secrets + .filter((el) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: groupedPaths[el.folderId][0].environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) ) - ); + .map((secret) => + reshapeBridgeSecret( + projectId, + groupedPaths[secret.folderId][0].environment, + groupedPaths[secret.folderId][0].path, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + } + ) + ); return decryptedSecrets; }; @@ -653,6 +754,8 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + let paths: { folderId: string; path: string }[] = []; if (recursive) { @@ -661,26 +764,13 @@ export const secretV2BridgeServiceFactory = ({ projectEnvDAL, projectId, environment, - currentPath: path, - hasAccess: (permissionEnvironment, permissionSecretPath) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: permissionEnvironment, - secretPath: permissionSecretPath - }) - ) + currentPath: path }); if (!deepPaths) return { secrets: [], imports: [] }; paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); - const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return { secrets: [], imports: [] }; @@ -701,27 +791,44 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - const decryptedSecrets = secrets.map((secret) => - reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }) - ); + const decryptedSecrets = secrets + .filter((el) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) + ) + .map((secret) => + reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }) + ); const expandSecretReferences = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath) => + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) ) }); @@ -752,26 +859,24 @@ export const secretV2BridgeServiceFactory = ({ } const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); - const allowedImports = secretImports.filter(({ importEnv, importPath, isReplication }) => - !isReplication && - // if its service token allow full access over imported one - actor === ActorType.SERVICE - ? true - : permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) - ); + const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ - allowedImports, + secretImports: allowedImports, secretDAL, folderDAL, secretImportDAL, expandSecretReferences, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ) }); return { @@ -801,10 +906,7 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new NotFoundError({ @@ -840,17 +942,43 @@ export const secretV2BridgeServiceFactory = ({ userId: secretType === SecretType.Personal ? actorId : null, key: secretName }) - .then((el) => SecretsV2Schema.parse({ ...el, id: el.secretId }))); + .then((el) => + SecretsV2Schema.extend({ + tags: z + .object({ slug: z.string(), name: z.string(), id: z.string(), color: z.string() }) + .array() + .default([]) + .optional() + }).parse({ + ...el, + id: el.secretId + }) + )); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ); const expandSecretReferences = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath) => + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) ) }); @@ -859,25 +987,23 @@ export const secretV2BridgeServiceFactory = ({ // here we consider the import order also thus starting from bottom if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); - const allowedImports = secretImports.filter(({ importEnv, importPath }) => - // if its service token allow full access over imported one - actor === ActorType.SERVICE - ? true - : permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) - ); const importedSecrets = await fnSecretsV2FromImports({ - allowedImports, + secretImports, secretDAL, folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined + expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ) }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { @@ -935,10 +1061,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -948,20 +1070,68 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secrets = await secretDAL.findBySecretKeys( + const secrets = await secretDAL.find({ folderId, - inputSecrets.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: inputSecrets.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secrets.length) throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); // get all tags const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; - if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "One or more tags not found" }); + if (tags.length !== sanitizedTagIds.length) + throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); + const tagsGroupByID = groupBy(tags, (i) => i.id); + + inputSecrets.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.secretKey, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + + // now get all secret references made and validate the permission + const secretReferencesGroupByInputSecretKey: Record> = {}; + const secretReferences: TSecretReference[] = []; + inputSecrets.forEach((el) => { + if (el.secretValue) { + const references = getAllSecretReferences(el.secretValue); + secretReferencesGroupByInputSecretKey[el.secretKey] = references; + secretReferences.push(...references.nestedReferences); + references.localReferences.forEach((localRefKey) => { + secretReferences.push({ secretKey: localRefKey, secretPath, environment }); + }); + } + }); + await $validateSecretReferences(projectId, permission, secretReferences); const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); @@ -969,16 +1139,7 @@ export const secretV2BridgeServiceFactory = ({ const newSecrets = await secretDAL.transaction(async (tx) => fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { - const references = getAllNestedSecretReferences(el.secretValue); - references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); + const references = secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences; return { version: 1, @@ -1040,10 +1201,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1053,38 +1210,128 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secretsToUpdate = await secretDAL.findBySecretKeys( + const secretsToUpdate = await secretDAL.find({ folderId, - inputSecrets.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: inputSecrets.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secretsToUpdate.length !== inputSecrets.length) throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` }); const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key); + secretsToUpdate.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + }); + + // get all tags + const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); + const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; + if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); + const tagsGroupByID = groupBy(tags, (i) => i.id); + + // check again to avoid non authorized tags are removed + inputSecrets.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.secretKey, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + // now find any secret that needs to update its name // same process as above const secretsWithNewName = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName)); if (secretsWithNewName.length) { - const secrets = await secretDAL.findBySecretKeys( + const secrets = await secretDAL.find({ folderId, - secretsWithNewName.map((el) => ({ - key: el.newSecretName as string, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: secretsWithNewName.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secrets.length) throw new BadRequestError({ message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}` }); - } - // get all tags - const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); - const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; - if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "One or more tags not found" }); + secretsWithNewName.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.newSecretName as string, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + } + // now get all secret references made and validate the permission + const secretReferencesGroupByInputSecretKey: Record> = {}; + const secretReferences: TSecretReference[] = []; + inputSecrets.forEach((el) => { + if (el.secretValue) { + const references = getAllSecretReferences(el.secretValue); + secretReferencesGroupByInputSecretKey[el.secretKey] = references; + secretReferences.push(...references.nestedReferences); + references.localReferences.forEach((localRefKey) => { + secretReferences.push({ secretKey: localRefKey, secretPath, environment }); + }); + } + }); + await $validateSecretReferences(projectId, permission, secretReferences); const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); @@ -1099,22 +1346,10 @@ export const secretV2BridgeServiceFactory = ({ typeof el.secretValue !== "undefined" ? { encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, - references: getAllNestedSecretReferences(el.secretValue) + references: secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences } : {}; - if (encryptedValue.references) { - encryptedValue.references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); - } - return { filter: { id: originalSecret.id, type: SecretType.Shared }, data: { @@ -1172,10 +1407,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1185,17 +1416,47 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secretsToDelete = await secretDAL.findBySecretKeys( + const secretsToDelete = await secretDAL.find({ folderId, - inputSecrets.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: inputSecrets.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secretsToDelete.length !== inputSecrets.length) throw new NotFoundError({ message: `One or more secrets does not exist: ${secretsToDelete.map((el) => el.key).join(", ")}` }); + secretsToDelete.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + }) + ); + }); const secretsDeleted = await secretDAL.transaction(async (tx) => fnSecretBulkDelete({ @@ -1304,7 +1565,8 @@ export const secretV2BridgeServiceFactory = ({ .map(({ id, encryptedValue }) => ({ secretId: id, references: encryptedValue - ? getAllNestedSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString()) + ? getAllSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString()) + .nestedReferences : [] })), tx @@ -1335,21 +1597,6 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) - ); - const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath); if (!sourceFolder) { throw new NotFoundError({ @@ -1372,9 +1619,20 @@ export const secretV2BridgeServiceFactory = ({ const sourceSecrets = await secretDAL.find({ type: SecretType.Shared, $in: { - id: secretIds + [`${TableName.SecretV2}.id` as "id"]: secretIds } }); + sourceSecrets.forEach((secret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + }); if (sourceSecrets.length !== secretIds.length) { throw new BadRequestError({ @@ -1445,6 +1703,32 @@ export const secretV2BridgeServiceFactory = ({ message: "Selected secrets already exist in the destination." }); } + + // permission check whether can create or edit the ones in the destination folder + locallyCreatedSecrets.forEach((secret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationEnvironment, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + }); + + locallyUpdatedSecrets.forEach((secret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationEnvironment, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + }); + const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( projectId, destinationFolder.environment.slug, @@ -1511,7 +1795,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, reminderRepeatDays: doc.reminderRepeatDays, - references: doc.value ? getAllNestedSecretReferences(doc.value) : [] + references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] }; }) }); @@ -1540,7 +1824,7 @@ export const secretV2BridgeServiceFactory = ({ ...(doc.encryptedValue ? { encryptedValue: doc.encryptedValue, - references: doc.value ? getAllNestedSecretReferences(doc.value) : [] + references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] } : { encryptedValue: undefined, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index a76c57561..0bb93198d 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -15,6 +15,12 @@ type TPartialSecret = Pick; +export type TSecretReferenceDTO = { + environment: string; + secretPath: string; + secretKey: string; +}; + export type TGetSecretsDTO = { expandSecretReferences?: boolean; path: string; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 70a4182cd..65691fcbb 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -25,7 +25,7 @@ import { logger } from "@app/lib/logger"; import { fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, - getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge + getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; @@ -793,7 +793,7 @@ export const createManySecretsRawFnFactory = ({ : null, skipMultilineEncoding: secret.skipMultilineEncoding, tags: secret.tags, - references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) + references: getAllSecretReferences(secret.secretValue).nestedReferences }; }); @@ -973,7 +973,7 @@ export const updateManySecretsRawFnFactory = ({ : null, skipMultilineEncoding: secret.skipMultilineEncoding, tags: secret.tags, - references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) + references: getAllSecretReferences(secret.secretValue).nestedReferences }; }); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 688a196aa..f98a78252 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -50,7 +50,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; -import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; +import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; @@ -342,7 +342,8 @@ export const secretQueueFactory = ({ secretDAL: secretV2BridgeDAL, expandSecretReferences, secretImportDAL, - allowedImports: secretImports + secretImports, + hasSecretAccess: () => true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { @@ -1147,7 +1148,7 @@ export const secretQueueFactory = ({ : ""; const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob; // create references - const references = getAllNestedSecretReferences(value); + const references = getAllSecretReferences(value).nestedReferences; secretReferences.push({ secretId: el.id, references }); const encryptedComment = comment diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index ea8c2a4a9..eaa5f5c4e 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -2436,17 +2436,26 @@ export const secretServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath + }) ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath + }) ); const { botKey } = await projectBotService.getBotKey(project.id); diff --git a/frontend/src/components/permissions/GlobPermissionInfo.tsx b/frontend/src/components/permissions/GlobPermissionInfo.tsx new file mode 100644 index 000000000..b65b0c307 --- /dev/null +++ b/frontend/src/components/permissions/GlobPermissionInfo.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; +import picomatch from "picomatch"; + +import { FormControl } from "../v2/FormControl"; +import { Input } from "../v2/Input"; + +export const GlobPermissionInfo = () => { + const [pattern, setPattern] = useState(""); + const [text, setText] = useState(""); + + return ( +
+
A glob pattern uses wildcards to match resources or paths.
+
+ + setPattern(e.target.value)} /> + +
+
+ + setText(e.target.value)} /> + +
+
+ ); +}; diff --git a/frontend/src/components/permissions/ProjectPermissionCan.tsx b/frontend/src/components/permissions/ProjectPermissionCan.tsx index f1af141f2..4fc5acde9 100644 --- a/frontend/src/components/permissions/ProjectPermissionCan.tsx +++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx @@ -1,23 +1,25 @@ import { FunctionComponent, ReactNode } from "react"; -import { BoundCanProps, Can } from "@casl/react"; +import { AbilityTuple, MongoAbility } from "@casl/ability"; +import { Can } from "@casl/react"; -import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext"; +import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext"; -import { Tooltip } from "../v2"; +import { Tooltip } from "../v2/Tooltip"; -type Props = { +type Props = { label?: ReactNode; // this prop is used when there exist already a tooltip as helper text for users // so when permission is allowed same tooltip will be reused to show helpertext renderTooltip?: boolean; allowedLabel?: string; - // BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission - // For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error - // tried a lot combinations - // REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15 -} & BoundCanProps; + children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode); + passThrough?: boolean; + I: T[0]; + a: T[1]; + ability?: MongoAbility; +}; -export const ProjectPermissionCan: FunctionComponent = ({ +export const ProjectPermissionCan: FunctionComponent> = ({ label = "Access restricted", children, passThrough = true, @@ -31,9 +33,7 @@ export const ProjectPermissionCan: FunctionComponent = ({ {(isAllowed, ability) => { // akhilmhdh: This is set as type due to error in casl react type. const finalChild = - typeof children === "function" - ? children(isAllowed, ability as TProjectPermission) - : children; + typeof children === "function" ? children(isAllowed, ability as any) : children; if (!isAllowed && passThrough) { return {finalChild}; diff --git a/frontend/src/components/permissions/index.tsx b/frontend/src/components/permissions/index.tsx index 8d523c311..5103b0f73 100644 --- a/frontend/src/components/permissions/index.tsx +++ b/frontend/src/components/permissions/index.tsx @@ -1,3 +1,4 @@ +export { GlobPermissionInfo } from "./GlobPermissionInfo"; export { OrgPermissionCan } from "./OrgPermissionCan"; export { PermissionDeniedBanner } from "./PermissionDeniedBanner"; export { ProjectPermissionCan } from "./ProjectPermissionCan"; diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index dcc7da62c..ce3f03d01 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -12,6 +12,7 @@ type Props = { placeholder?: string; className?: string; dropdownContainerClassName?: string; + containerClassName?: string; isLoading?: boolean; position?: "item-aligned" | "popper"; isDisabled?: boolean; @@ -31,12 +32,13 @@ export const Select = forwardRef( isDisabled, dropdownContainerClassName, position, + containerClassName, ...props }, ref ): JSX.Element => { return ( -
+
{ diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index afad2cd86..4b04f5cb8 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -3,5 +3,6 @@ export type { ProjectPermissionSet, TProjectPermission } from "./types"; export { ProjectPermissionActions, ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d47a3d01b..307ef74af 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -7,6 +7,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionDynamicSecretActions { + ReadRootCredential = "read-root-credential", + CreateRootCredential = "create-root-credential", + EditRootCredential = "edit-root-credential", + DeleteRootCredential = "delete-root-credential", + Lease = "lease" +} + export enum ProjectPermissionCmekActions { Read = "read", Create = "create", @@ -21,7 +29,7 @@ export enum PermissionConditionOperators { $ALL = "$all", $REGEX = "$regex", $EQ = "$eq", - $NEQ = "$neq", + $NEQ = "$ne", $GLOB = "$glob" } @@ -37,7 +45,7 @@ export type TPermissionConditionOperators = { export type TPermissionCondition = Record< string, | string - | { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string } + | { $in: string[]; $all: string[]; $regex: string; $eq: string; $ne: string; $glob: string } >; export enum ProjectPermissionSub { @@ -52,9 +60,11 @@ export enum ProjectPermissionSub { Tags = "tags", AuditLogs = "audit-logs", IpAllowList = "ip-allowlist", - Workspace = "workspace", + Project = "workspace", Secrets = "secrets", SecretFolders = "secret-folders", + SecretImports = "secret-imports", + DynamicSecrets = "dynamic-secrets", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", @@ -68,7 +78,24 @@ export enum ProjectPermissionSub { Cmek = "cmek" } -type SubjectFields = { +export type SecretSubjectFields = { + environment: string; + secretPath: string; + secretName: string; + secretTags: string[]; +}; + +export type SecretFolderSubjectFields = { + environment: string; + secretPath: string; +}; + +export type DynamicSecretSubjectFields = { + environment: string; + secretPath: string; +}; + +export type SecretImportSubjectFields = { environment: string; secretPath: string; }; @@ -76,13 +103,30 @@ type SubjectFields = { export type ProjectPermissionSet = | [ ProjectPermissionActions, - ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) + ( + | ProjectPermissionSub.Secrets + | (ForcedSubject & SecretSubjectFields) + ) ] | [ ProjectPermissionActions, ( | ProjectPermissionSub.SecretFolders - | (ForcedSubject & SubjectFields) + | (ForcedSubject & SecretFolderSubjectFields) + ) + ] + | [ + ProjectPermissionDynamicSecretActions, + ( + | ProjectPermissionSub.DynamicSecrets + | (ForcedSubject & DynamicSecretSubjectFields) + ) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & SecretImportSubjectFields) ) ] | [ProjectPermissionActions, ProjectPermissionSub.Role] @@ -95,19 +139,19 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Environments] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.Settings] - | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] + | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] - | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] - | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] - | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]; - + | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; export type TProjectPermission = MongoAbility; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index ada436833..91dae5d2d 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -11,6 +11,7 @@ export type { TProjectPermission } from "./ProjectPermissionContext"; export { ProjectPermissionActions, ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, ProjectPermissionProvider, ProjectPermissionSub, useProjectPermission diff --git a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx index 103ff61b7..22c91ab52 100644 --- a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx +++ b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx @@ -1,31 +1,29 @@ import { ComponentType } from "react"; -import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability"; +import { AbilityTuple } from "@casl/ability"; import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { TProjectPermission, useProjectPermission } from "@app/context"; +import { useProjectPermission } from "@app/context"; +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; -type Props = (T extends AbilityTuple - ? { - action: T[0]; - subject: Extract; - } - : { - action: string; - subject: string; - }) & { className?: string; containerClassName?: string }; +type Props = { + className?: string; + containerClassName?: string; + action: T[0]; + subject: T[1]; +}; -export const withProjectPermission = ( - Component: ComponentType, - { action, subject, className, containerClassName }: Props["abilities"]> +export const withProjectPermission = ( + Component: ComponentType, "action" | "subject"> & T>, + { action, subject, className, containerClassName }: Props ) => { - const HOC = (hocProps: T) => { + const HOC = (hocProps: Omit, "action" | "subject"> & T) => { const { permission } = useProjectPermission(); // akhilmhdh: Set as any due to casl/react ts type bug // REASON: casl due to its type checking can't seem to union even if union intersection is applied - if (permission.cannot(action as any, subject)) { + if (permission.cannot(action as any, subject as any)) { return (
["dashboard"] as const, @@ -154,10 +155,20 @@ export const useGetProjectSecretsOverview = ( }, select: useCallback((data: Awaited>) => { const { secrets, ...select } = data; + const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : []; + + const uniqueFolders = select.folders ? unique(select.folders, (i) => i.name) : []; + + const uniqueDynamicSecrets = select.dynamicSecrets + ? unique(select.dynamicSecrets, (i) => i.name) + : []; return { ...select, - secrets: secrets ? mergePersonalSecrets(secrets) : undefined + secrets: secrets ? mergePersonalSecrets(secrets) : undefined, + totalUniqueSecretsInPage: uniqueSecrets.length, + totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length, + totalUniqueFoldersInPage: uniqueFolders.length }; }, []), keepPreviousData: true diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 08e75ee3b..865d8541d 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -12,6 +12,9 @@ export type DashboardProjectSecretsOverviewResponse = { totalFolderCount?: number; totalDynamicSecretCount?: number; totalCount: number; + totalUniqueSecretsInPage: number; + totalUniqueDynamicSecretsInPage: number; + totalUniqueFoldersInPage: number; }; export type DashboardProjectSecretsDetailsResponse = { diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx index fb21a425e..2ee6519f6 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx @@ -1,4 +1,3 @@ -import { packRules } from "@casl/ability/extra"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -16,10 +15,7 @@ export const useCreateProjectUserAdditionalPrivilege = () => { return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({ mutationFn: async (dto) => { - const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", { - ...dto, - permissions: packRules(dto.permissions) - }); + const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", dto); return data.privilege; }, onSuccess: (_, { projectMembershipId }) => { @@ -35,7 +31,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => { mutationFn: async (dto) => { const { data } = await apiRequest.patch( `/api/v1/additional-privilege/users/${dto.privilegeId}`, - { ...dto, permissions: dto.permissions ? packRules(dto.permissions) : undefined } + dto ); return data.privilege; }, diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx index 41c9a0dcc..261821c1f 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx @@ -1,4 +1,3 @@ -import { PackRule, unpackRules } from "@casl/ability/extra"; import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -18,10 +17,7 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => { } = await apiRequest.get<{ privilege: Omit & { permissions: unknown }; }>(`/api/v1/additional-privilege/users/${privilegeId}`); - return { - ...privilege, - permissions: unpackRules(privilege.permissions as PackRule[]) - }; + return privilege; }; export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => { @@ -44,7 +40,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => { }>("/api/v1/additional-privilege/users", { params: { projectMembershipId } }); return privileges.map((el) => ({ ...el, - permissions: unpackRules(el.permissions as PackRule[]) + permissions: el.permissions as TProjectPermission[] })); } }); diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx index b757a07ab..da40005c2 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx @@ -4,6 +4,15 @@ export enum ProjectUserAdditionalPrivilegeTemporaryMode { Relative = "relative" } +export type TProjectSpecificPrivilegePermission = { + conditions: { + environment: string; + secretPath?: { $glob: string }; + }; + actions: string[]; + subject: string; +}; + export type TProjectUserPrivilege = { projectMembershipId: string; slug: string; @@ -12,21 +21,21 @@ export type TProjectUserPrivilege = { updatedAt: Date; permissions?: TProjectPermission[]; } & ( - | { + | { isTemporary: true; temporaryMode: string; temporaryRange: string; temporaryAccessStartTime: string; temporaryAccessEndTime?: string; } - | { + | { isTemporary: false; temporaryMode?: null; temporaryRange?: null; temporaryAccessStartTime?: null; temporaryAccessEndTime?: null; } - ); +); export type TCreateProjectUserPrivilegeDTO = { projectMembershipId: string; @@ -35,7 +44,7 @@ export type TCreateProjectUserPrivilegeDTO = { temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode; temporaryRange?: string; temporaryAccessStartTime?: string; - permissions: TProjectPermission[]; + permissions: TProjectSpecificPrivilegePermission; }; export type TUpdateProjectUserPrivlegeDTO = { diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 3f782171e..1c6b85b66 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -22,7 +22,7 @@ export const useCreateProjectRole = () => { mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto); + } = await apiRequest.post(`/api/v2/workspace/${projectSlug}/roles`, dto); return role; }, onSuccess: (_, { projectSlug }) => { @@ -38,7 +38,7 @@ export const useUpdateProjectRole = () => { mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto); + } = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/roles/${id}`, dto); return role; }, onSuccess: (_, { projectSlug }) => { @@ -53,7 +53,7 @@ export const useDeleteProjectRole = () => { mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => { const { data: { role } - } = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`); + } = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/roles/${id}`); return role; }, onSuccess: (_, { projectSlug }) => { diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index 77e82e4fa..1726abfd3 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -7,6 +7,8 @@ import picomatch from "picomatch"; import { apiRequest } from "@app/config/request"; import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types"; +import { groupBy } from "@app/lib/fn/array"; +import { omit } from "@app/lib/fn/object"; import { OrgUser, TProjectMembership } from "../users/types"; import { @@ -49,7 +51,7 @@ export const roleQueryKeys = { export const getProjectRoles = async (projectId: string) => { const { data } = await apiRequest.get<{ roles: Array> }>( - `/api/v1/workspace/${projectId}/roles` + `/api/v2/workspace/${projectId}/roles` ); return data.roles; }; @@ -66,7 +68,7 @@ export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) = queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug), queryFn: async () => { const { data } = await apiRequest.get<{ role: TProjectRole }>( - `/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}` + `/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}` ); return data.role; }, @@ -134,7 +136,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss permissions: PackRule>>[]; membership: Omit & { roles: { role: string }[] }; }; - }>(`/api/v1/workspace/${workspaceId}/permissions`, {}); + }>(`/api/v2/workspace/${workspaceId}/permissions`, {}); return data.data; }; @@ -146,8 +148,32 @@ export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPer enabled: Boolean(workspaceId), select: (data) => { const rule = unpackRules>>(data.permissions); - const ability = createMongoAbility(rule, { conditionsMatcher }); + const negatedRules = groupBy( + rule.filter((i) => i.inverted && i.conditions), + (i) => `${i.subject}-${JSON.stringify(i.conditions)}` + ); + const ability = createMongoAbility(rule, { + // this allows in frontend to skip some rules using * + conditionsMatcher: (rules) => { + return (entity) => { + // skip validation if its negated rules + const isNegatedRule = + // eslint-disable-next-line no-underscore-dangle + negatedRules?.[`${entity.__caslSubjectType__}-${JSON.stringify(rules)}`]; + if (isNegatedRule) { + const baseMatcher = conditionsMatcher(rules); + return baseMatcher(entity); + } + const rulesStrippedOfWildcard = omit( + rules, + Object.keys(entity).filter((el) => entity[el]?.includes("*")) + ); + const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard); + return baseMatcher(entity); + }; + } + }); const membership = { ...data.membership, roles: data.membership.roles.map(({ role }) => role) diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index da28d9ca8..50cdf5a22 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -40,6 +40,7 @@ export type TPermission = { export type TProjectPermission = { conditions?: Record; + inverted?: boolean; action: string | string[]; subject: string | string[]; }; diff --git a/frontend/src/lib/fn/array.ts b/frontend/src/lib/fn/array.ts index b92390697..eef80a892 100644 --- a/frontend/src/lib/fn/array.ts +++ b/frontend/src/lib/fn/array.ts @@ -13,3 +13,22 @@ export const groupBy = ( acc[groupId].push(item); return acc; }, {} as Record); + +/** + * Given a list of items returns a new list with only + * unique items. Accepts an optional identity function + * to convert each item in the list to a comparable identity + * value + */ +export const unique = ( + array: readonly T[], + toKey?: (item: T) => K +): T[] => { + const valueMap = array.reduce((acc, item) => { + const key = toKey ? toKey(item) : (item as unknown as string | number | symbol); + if (acc[key]) return acc; + acc[key] = item; + return acc; + }, {} as Record); + return Object.values(valueMap); +}; diff --git a/frontend/src/lib/fn/object.ts b/frontend/src/lib/fn/object.ts new file mode 100644 index 000000000..0af1ead2d --- /dev/null +++ b/frontend/src/lib/fn/object.ts @@ -0,0 +1,20 @@ +/** + * Omit a list of properties from an object + * returning a new object with the properties + * that remain + */ +export const omit = (obj: T, keys: TKeys[]): Omit => { + if (!obj) return {} as Omit; + if (!keys || keys.length === 0) return obj as Omit; + return keys.reduce( + (acc, key) => { + // Gross, I know, it's mutating the object, but we + // are allowing it in this very limited scope due + // to the performance implications of an omit func. + // Not a pattern or practice to use elsewhere. + delete acc[key]; + return acc; + }, + { ...obj } + ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx index 7894d0e78..51581de4d 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -184,20 +184,20 @@ export const SpecificPrivilegeSecretForm = ({ { action: ProjectPermissionActions.Delete, allowed: data.delete }, { action: ProjectPermissionActions.Edit, allowed: data.edit } ]; - const conditions: Record = { environment: data.environmentSlug }; + const conditions: { environment: string; secretPath?: { $glob: string } } = { + environment: data.environmentSlug + }; if (data.secretPath) { conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) }; } await updateUserPrivilege.mutateAsync({ privilegeId: privilege.id, ...data.temporaryAccess, - permissions: actions - .filter(({ allowed }) => allowed) - .map(({ action }) => ({ - action, - subject: [ProjectPermissionSub.Secrets], - conditions - })), + permissions: { + subject: ProjectPermissionSub.Secrets, + conditions, + actions: actions.filter((i) => i.allowed).map((i) => i.action) + }, projectMembershipId: privilege.projectMembershipId }); createNotification({ @@ -642,15 +642,13 @@ export const SpecificPrivilegeSection = ({ membershipId }: Props) => { if (createUserPrivilege.isLoading) return; try { await createUserPrivilege.mutateAsync({ - permissions: [ - { - action: ProjectPermissionActions.Read, - subject: [ProjectPermissionSub.Secrets], - conditions: { - environment: currentWorkspace?.environments?.[0].slug - } + permissions: { + actions: [ProjectPermissionActions.Read], + subject: ProjectPermissionSub.Secrets, + conditions: { + environment: currentWorkspace?.environments?.[0].slug || "" } - ], + }, projectMembershipId: membershipId }); createNotification({ diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts index 7dd981691..5832e5a14 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts @@ -7,6 +7,7 @@ import { } from "@app/context"; import { PermissionConditionOperators, + ProjectPermissionDynamicSecretActions, TPermissionCondition, TPermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; @@ -28,8 +29,12 @@ const CmekPolicyActionSchema = z.object({ decrypt: z.boolean().optional() }); -const SecretFolderPolicyActionSchema = z.object({ - read: z.boolean().optional() +const DynamicSecretPolicyActionSchema = z.object({ + [ProjectPermissionDynamicSecretActions.ReadRootCredential]: z.boolean().optional(), + [ProjectPermissionDynamicSecretActions.EditRootCredential]: z.boolean().optional(), + [ProjectPermissionDynamicSecretActions.DeleteRootCredential]: z.boolean().optional(), + [ProjectPermissionDynamicSecretActions.CreateRootCredential]: z.boolean().optional(), + [ProjectPermissionDynamicSecretActions.Lease]: z.boolean().optional() }); const SecretRollbackPolicyActionSchema = z.object({ @@ -42,11 +47,29 @@ const WorkspacePolicyActionSchema = z.object({ delete: z.boolean().optional() }); -const ConditionSchema = z.object({ - operator: z.string(), - lhs: z.string(), - rhs: z.string().min(1) -}); +const ConditionSchema = z + .object({ + operator: z.string(), + lhs: z.string(), + rhs: z.string().min(1) + }) + .array() + .optional() + .default([]) + .refine( + (el) => { + const lhsOperatorSet = new Set(); + for (let i = 0; i < el.length; i += 1) { + const { lhs, operator } = el[i]; + if (lhsOperatorSet.has(`${lhs}-${operator}`)) { + return false; + } + lhsOperatorSet.add(`${lhs}-${operator}`); + } + return true; + }, + { message: "Duplicate operator found for a condition" } + ); export const formSchema = z.object({ name: z.string().trim(), @@ -59,27 +82,29 @@ export const formSchema = z.object({ permissions: z .object({ [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ - conditions: ConditionSchema.array() - .optional() - .default([]) - .refine( - (el) => { - const lhsOperatorSet = new Set(); - for (let i = 0; i < el.length; i += 1) { - const { lhs, operator } = el[i]; - if (lhsOperatorSet.has(`${lhs}-${operator}`)) { - return false; - } - lhsOperatorSet.add(`${lhs}-${operator}`); - } - return true; - }, - { message: "Duplicate operator found for a condition" } - ) + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.DynamicSecrets]: DynamicSecretPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema }) .array() .default([]), - [ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]), @@ -98,7 +123,7 @@ export const formSchema = z.object({ [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]), - [ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]), + [ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]), [ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]), @@ -110,8 +135,22 @@ export const formSchema = z.object({ export type TFormSchema = z.infer; +type TConditionalFields = + | ProjectPermissionSub.Secrets + | ProjectPermissionSub.SecretFolders + | ProjectPermissionSub.SecretImports + | ProjectPermissionSub.DynamicSecrets; + +export const isConditionalSubjects = ( + subject: ProjectPermissionSub +): subject is TConditionalFields => + subject === (ProjectPermissionSub.Secrets as const) || + subject === ProjectPermissionSub.DynamicSecrets || + subject === ProjectPermissionSub.SecretImports || + subject === ProjectPermissionSub.SecretFolders; + const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { - const formConditions: z.infer[] = []; + const formConditions: z.infer = []; Object.entries(caslConditions).forEach(([type, condition]) => { if (typeof condition === "string") { formConditions.push({ @@ -138,12 +177,15 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const formVal: Partial = {}; permissions.forEach((permission) => { - const { subject: caslSub, action, conditions } = permission; + const { subject: caslSub, action, conditions, inverted } = permission; const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub; if ( [ ProjectPermissionSub.Secrets, + ProjectPermissionSub.DynamicSecrets, + ProjectPermissionSub.SecretFolders, + ProjectPermissionSub.SecretImports, ProjectPermissionSub.Member, ProjectPermissionSub.Groups, ProjectPermissionSub.Identity, @@ -166,37 +208,67 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.Kms ].includes(subject) ) { - const canRead = action.includes(ProjectPermissionActions.Read); - const canEdit = action.includes(ProjectPermissionActions.Edit); - const canDelete = action.includes(ProjectPermissionActions.Delete); - const canCreate = action.includes(ProjectPermissionActions.Create); - // from above statement we are sure it won't be undefined - if (subject === ProjectPermissionSub.Secrets) { + if (isConditionalSubjects(subject)) { if (!formVal[subject]) formVal[subject] = []; - formVal[subject]!.push({ - read: canRead, - create: canCreate, - edit: canEdit, - delete: canDelete, - conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [] - }); + + if (subject === ProjectPermissionSub.DynamicSecrets) { + const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential); + const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential); + const canDelete = action.includes( + ProjectPermissionDynamicSecretActions.DeleteRootCredential + ); + const canCreate = action.includes( + ProjectPermissionDynamicSecretActions.CreateRootCredential + ); + const canLease = action.includes(ProjectPermissionDynamicSecretActions.Lease); + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + [ProjectPermissionDynamicSecretActions.ReadRootCredential]: canRead, + [ProjectPermissionDynamicSecretActions.CreateRootCredential]: canCreate, + [ProjectPermissionDynamicSecretActions.EditRootCredential]: canEdit, + [ProjectPermissionDynamicSecretActions.DeleteRootCredential]: canDelete, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted, + [ProjectPermissionDynamicSecretActions.Lease]: canLease + }); + } else { + // for other subjects + const canRead = action.includes(ProjectPermissionActions.Read); + const canEdit = action.includes(ProjectPermissionActions.Edit); + const canDelete = action.includes(ProjectPermissionActions.Delete); + const canCreate = action.includes(ProjectPermissionActions.Create); + formVal[subject]!.push({ + read: canRead, + create: canCreate, + edit: canEdit, + delete: canDelete, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + } } else { // deduplicate multiple rules for other policies // because they don't have condition it doesn't make sense for multiple rules + const canRead = action.includes(ProjectPermissionActions.Read); + const canEdit = action.includes(ProjectPermissionActions.Edit); + const canDelete = action.includes(ProjectPermissionActions.Delete); + const canCreate = action.includes(ProjectPermissionActions.Create); + if (!formVal[subject]) formVal[subject] = [{}]; if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true; if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true; if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true; } - } else if (subject === ProjectPermissionSub.Workspace) { + } else if (subject === ProjectPermissionSub.Project) { const canEdit = action.includes(ProjectPermissionActions.Edit); const canDelete = action.includes(ProjectPermissionActions.Delete); if (!formVal[subject]) formVal[subject] = [{}]; // from above statement we are sure it won't be undefined - if (canEdit) formVal[subject as ProjectPermissionSub.Workspace]![0].edit = true; + if (canEdit) formVal[subject as ProjectPermissionSub.Project]![0].edit = true; if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true; } else if (subject === ProjectPermissionSub.SecretRollback) { const canRead = action.includes(ProjectPermissionActions.Read); @@ -206,12 +278,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { // from above statement we are sure it won't be undefined if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true; - } else if (subject === ProjectPermissionSub.SecretFolders) { - const canRead = action.includes(ProjectPermissionActions.Read); - if (!formVal[subject]) formVal[subject] = [{}]; - - // from above statement we are sure it won't be undefined - if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; } else if (subject === ProjectPermissionSub.Cmek) { const canRead = action.includes(ProjectPermissionCmekActions.Read); const canEdit = action.includes(ProjectPermissionCmekActions.Edit); @@ -264,7 +330,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { Object.entries(formVal || {}).forEach(([subject, rules]) => { rules.forEach((actions) => { const caslActions = Object.keys(actions).filter( - (el) => actions?.[el as keyof typeof actions] && el !== "conditions" + (el) => actions?.[el as keyof typeof actions] && el !== "conditions" && el !== "inverted" ); const caslConditions = "conditions" in actions @@ -274,6 +340,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { permissions.push({ action: caslActions, subject, + inverted: (actions as { inverted?: boolean })?.inverted, conditions: caslConditions }); }); @@ -288,7 +355,7 @@ export type TProjectPermissionObject = { label: string; value: keyof Omit< NonNullable[K]>[number], - "conditions" + "conditions" | "inverted" >; }[]; }; @@ -306,7 +373,42 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { }, [ProjectPermissionSub.SecretFolders]: { title: "Secret Folders", - actions: [{ label: "Read Only", value: "read" }] + actions: [ + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, + [ProjectPermissionSub.SecretImports]: { + title: "Secret Imports", + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, + [ProjectPermissionSub.DynamicSecrets]: { + title: "Dynamic Secrets", + actions: [ + { + label: "Read root credentials", + value: ProjectPermissionDynamicSecretActions.ReadRootCredential + }, + { + label: "Create root credentials", + value: ProjectPermissionDynamicSecretActions.CreateRootCredential + }, + { + label: "Modify root credentials", + value: ProjectPermissionDynamicSecretActions.EditRootCredential + }, + { + label: "Remove root credentials", + value: ProjectPermissionDynamicSecretActions.DeleteRootCredential + }, + { label: "Manage Leases", value: ProjectPermissionDynamicSecretActions.Lease } + ] }, [ProjectPermissionSub.Cmek]: { title: "KMS", @@ -332,7 +434,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, - [ProjectPermissionSub.Workspace]: { + [ProjectPermissionSub.Project]: { title: "Project", actions: [ { label: "Update project details", value: "edit" }, diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index fb3567897..a7ac06010 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -10,13 +10,15 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; -import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions"; +import { GeneralPermissionConditions } from "./components/GeneralPermissionConditions"; +import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies"; import { NewPermissionRule } from "./components/NewPermissionRule"; import { SecretPermissionConditions } from "./components/SecretPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; import { formRolePermission2API, formSchema, + isConditionalSubjects, PROJECT_PERMISSION_OBJECT, rolePermission2Form, TFormSchema @@ -27,6 +29,17 @@ type Props = { isDisabled?: boolean; }; +const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?: boolean) => { + if (subject === ProjectPermissionSub.Secrets) + return ; + + if (isConditionalSubjects(subject)) { + return ; + } + + return undefined; +}; + export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { currentWorkspace } = useWorkspace(); const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); @@ -130,17 +143,15 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
{!isLoading && } {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( - - {subject === ProjectPermissionSub.Secrets ? ( - - ) : undefined} - + {renderConditionalComponents(subject, isDisabled)} + ))}
diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx new file mode 100644 index 000000000..2ac518038 --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx @@ -0,0 +1,176 @@ +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + FormControl, + IconButton, + Input, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { + PermissionConditionOperators, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; + +import { TFormSchema } from "../ProjectRoleModifySection.utils"; +import { + getConditionOperatorHelperInfo, + renderOperatorSelectItems +} from "./PermissionConditionHelpers"; + +type Props = { + position?: number; + isDisabled?: boolean; + type: + | ProjectPermissionSub.DynamicSecrets + | ProjectPermissionSub.SecretFolders + | ProjectPermissionSub.SecretImports; +}; + +export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => { + const { + control, + watch, + formState: { errors } + } = useFormContext(); + const items = useFieldArray({ + control, + name: `permissions.${type}.${position}.conditions` + }); + + return ( +
+

Conditions

+

+ When this policy should apply (always if no conditions are added). +

+
+ {items.fields.map((el, index) => { + const condition = + (watch(`permissions.${type}.${position}.conditions.${index}`) as { + lhs: string; + rhs: string; + operator: string; + }) || {}; + return ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+ + + +
+
+
+ ( + + + + )} + /> +
+
+ items.remove(index)} + > + + +
+
+ ); + })} +
+ {errors?.permissions?.[type]?.[position]?.conditions?.message && ( +
+ + {errors?.permissions?.[type]?.[position]?.conditions?.message} +
+ )} +
{}
+
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx similarity index 66% rename from frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx rename to frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx index cdf0f5151..58c403579 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx @@ -1,14 +1,24 @@ import { cloneElement } from "react"; import { Controller, useFieldArray, useFormContext } from "react-hook-form"; -import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { + faChevronDown, + faChevronRight, + faInfoCircle, + faPlus, + faTrash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button, Checkbox, Tag } from "@app/components/v2"; +import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; -import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils"; +import { + isConditionalSubjects, + TFormSchema, + TProjectPermissionObject +} from "../ProjectRoleModifySection.utils"; type Props = { title: string; @@ -18,7 +28,7 @@ type Props = { isDisabled?: boolean; }; -export const GeneralPermissionOptions = >({ +export const GeneralPermissionPolicies = >({ subject, actions, children, @@ -63,6 +73,44 @@ export const GeneralPermissionOptions = {items.fields.map((el, rootIndex) => (
+ {isConditionalSubjects(subject) && ( +
+
Permission
+
+ ( + + )} + /> +
+
+ +

+ Whether to allow or forbid the selected actions when the following + conditions (if any) are met. +

+

Forbid rules must come after allow rules.

+ + } + > + +
+
+
+ )}
Actions
@@ -98,10 +146,10 @@ export const GeneralPermissionOptions = - {!isDisabled && subject === ProjectPermissionSub.Secrets && ( + {!isDisabled && isConditionalSubjects(subject) && (
diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 7bd6a9dd4..5f66e785b 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -12,6 +12,7 @@ import { PermissionDeniedBanner } from "@app/components/permissions"; import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, ProjectPermissionSub, useProjectPermission, useWorkspace @@ -37,7 +38,7 @@ import { ActionBar } from "./components/ActionBar"; import { CreateSecretForm } from "./components/CreateSecretForm"; import { PitDrawer } from "./components/PitDrawer"; import { SecretDropzone } from "./components/SecretDropzone"; -import { SecretListView } from "./components/SecretListView"; +import { SecretListView, SecretNoAccessListView } from "./components/SecretListView"; import { SnapshotView } from "./components/SnapshotView"; import { StoreProvider, @@ -83,8 +84,24 @@ const SecretMainPageContent = () => { const secretPath = (router.query.secretPath as string) || "/"; const canReadSecret = permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: "*", + secretTags: ["*"] + }) ); + + const canReadSecretImports = permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ); + + const canReadDynamicSecret = permission.can( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) + ); + const canDoReadRollback = permission.can( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback @@ -93,11 +110,12 @@ const SecretMainPageContent = () => { const defaultFilterState = { tags: {}, searchFilter: (router.query.searchFilter as string) || "", + // these should always be on by default for the UI, they will be disabled for the query below based off permissions include: { [RowType.Folder]: true, - [RowType.Import]: canReadSecret, - [RowType.DynamicSecret]: canReadSecret, - [RowType.Secret]: canReadSecret + [RowType.Import]: true, + [RowType.DynamicSecret]: true, + [RowType.Secret]: true } }; @@ -105,19 +123,6 @@ const SecretMainPageContent = () => { const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter); const [filterHistory, setFilterHistory] = useState>(new Map()); - // change filters if permissions change at different paths/env - useEffect(() => { - setFilter((prev) => ({ - ...prev, - include: { - [RowType.Folder]: true, - [RowType.Import]: canReadSecret, - [RowType.DynamicSecret]: canReadSecret, - [RowType.Secret]: canReadSecret - } - })); - }, [canReadSecret]); - useEffect(() => { if ( !isWorkspaceLoading && @@ -145,9 +150,9 @@ const SecretMainPageContent = () => { orderBy, search: debouncedSearchFilter, orderDirection, - includeImports: canReadSecret && filter.include.import, + includeImports: canReadSecretImports && filter.include.import, includeFolders: filter.include.folder, - includeDynamicSecrets: canReadSecret && filter.include.dynamic, + includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags }); @@ -210,8 +215,20 @@ const SecretMainPageContent = () => { isPaused: !canDoReadRollback }); + const noAccessSecretCount = Math.max( + (page * perPage > totalCount ? totalCount % perPage : perPage) - + (imports?.length || 0) - + (folders?.length || 0) - + (secrets?.length || 0) - + (dynamicSecrets?.length || 0), + 0 + ); const isNotEmpty = Boolean( - secrets?.length || folders?.length || imports?.length || dynamicSecrets?.length + secrets?.length || + folders?.length || + imports?.length || + dynamicSecrets?.length || + noAccessSecretCount ); const handleSortToggle = () => @@ -330,7 +347,6 @@ const SecretMainPageContent = () => { setFilter(defaultFilterState); setDebouncedSearchFilter(""); }; - return (
@@ -411,49 +427,53 @@ const SecretMainPageContent = () => {
Value
- )} - {canReadSecret && imports?.length && ( - - )} - {folders?.length && ( - - )} - {canReadSecret && dynamicSecrets?.length && ( - - )} - {canReadSecret && secrets?.length && ( - - )} - {!canReadSecret && folders?.length === 0 && } + )} + {canReadSecretImports && Boolean(imports?.length) && ( + + )} + {Boolean(folders?.length) && ( + + )} + {canReadDynamicSecret && Boolean(dynamicSecrets?.length) && ( + + )} + {canReadSecret && Boolean(secrets?.length) && ( + + )} + {canReadSecret && } + {!canReadSecret && + !canReadDynamicSecret && + !canReadSecretImports && + folders?.length === 0 && } +
-
{!isDetailsLoading && totalCount > 0 && ( - (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) - ); - const handleFolderCreate = async (folderName: string) => { try { await createFolder({ @@ -436,7 +430,12 @@ export const ActionBar = ({
{(isAllowed) => ( + + {(isAllowed) => ( + + )} + diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx index abef7b6c3..8953ab2bc 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx @@ -18,7 +18,7 @@ import { Tag, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteDynamicSecret } from "@app/hooks/api"; import { @@ -132,17 +132,27 @@ export const DynamicSecretListView = ({ )}
- + {(isAllowed) => ( + + )} + + {secret.status === DynamicSecretStatus.FailedDeletion && ( @@ -388,7 +419,12 @@ export const SecretDetailSidebar = ({ render={({ field: { value, onChange, onBlur } }) => ( {(isAllowed) => ( {(isAllowed) => (
- + {(isAllowed) => ( { )}
- + {(isAllowed) => (