diff --git a/backend/e2e-test/routes/v3/secret-reference.spec.ts b/backend/e2e-test/routes/v3/secret-reference.spec.ts index 560565342e..fb23cdc259 100644 --- a/backend/e2e-test/routes/v3/secret-reference.spec.ts +++ b/backend/e2e-test/routes/v3/secret-reference.spec.ts @@ -56,10 +56,7 @@ describe("Secret expansion", () => { } ]; - for (const secret of secrets) { - // eslint-disable-next-line no-await-in-loop - await createSecretV2(secret); - } + await Promise.all(secrets.map((el) => createSecretV2(el))); const expandedSecret = await getSecretByNameV2({ environmentSlug: seedData1.environment.slug, @@ -126,10 +123,7 @@ describe("Secret expansion", () => { } ]; - for (const secret of secrets) { - // eslint-disable-next-line no-await-in-loop - await createSecretV2(secret); - } + await Promise.all(secrets.map((el) => createSecretV2(el))); const expandedSecret = await getSecretByNameV2({ environmentSlug: seedData1.environment.slug, @@ -196,11 +190,7 @@ describe("Secret expansion", () => { } ]; - for (const secret of secrets) { - // eslint-disable-next-line no-await-in-loop - await createSecretV2(secret); - } - + await Promise.all(secrets.map((el) => createSecretV2(el))); const secretImportFromProdToDev = await createSecretImport({ environmentSlug: seedData1.environment.slug, workspaceId: projectId, @@ -285,11 +275,7 @@ describe("Secret expansion", () => { } ]; - for (const secret of secrets) { - // eslint-disable-next-line no-await-in-loop - await createSecretV2(secret); - } - + await Promise.all(secrets.map((el) => createSecretV2(el))); const secretImportFromProdToDev = await createSecretImport({ environmentSlug: seedData1.environment.slug, workspaceId: projectId, diff --git a/backend/package-lock.json b/backend/package-lock.json index 7d1f95ad81..dbd8c8073c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -3827,14 +3827,13 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "aix" @@ -5852,10 +5851,9 @@ } }, "node_modules/@probot/pino": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-2.4.0.tgz", - "integrity": "sha512-KUJ3eK2zLrPny7idWm9eQbBNhCJUjm1A1ttA6U4qiR2/ONWSffVlvr8oR26L59sVhoDkv1DOGmGPZS/bvSFisw==", - "license": "MIT", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-2.3.5.tgz", + "integrity": "sha512-IiyiNZonMw1dHC4EAdD55y5owV733d9Gll/IKsrLikB7EJ54+eMCOtL/qo+OmgWN9XV3NTDfziEQF2og/OBKog==", "dependencies": { "@sentry/node": "^6.0.0", "pino-pretty": "^6.0.0", @@ -5924,224 +5922,208 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "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==", + "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==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "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==", + "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==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "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==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", + "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "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==", + "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==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "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==", + "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==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "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==", + "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==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "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==", + "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==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "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==", + "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==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "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==", + "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==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "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==", + "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==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "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==", + "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==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "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==", + "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==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "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==", + "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==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "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==", + "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==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "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==", + "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==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "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==", + "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==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -7299,11 +7281,10 @@ } }, "node_modules/@types/estree": { - "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" + "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 }, "node_modules/@types/express": { "version": "4.17.21", @@ -8552,6 +8533,11 @@ "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", @@ -8861,15 +8847,36 @@ } }, "node_modules/avvio": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", - "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", - "license": "MIT", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.1.tgz", + "integrity": "sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==", "dependencies": { - "@fastify/error": "^3.3.0", - "fastq": "^1.17.1" + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.6.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", @@ -9728,10 +9735,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "engines": { "node": ">= 0.6" } @@ -10923,6 +10929,15 @@ "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", @@ -10944,6 +10959,15 @@ "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", @@ -11095,9 +11119,9 @@ } }, "node_modules/fastify": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.28.1.tgz", - "integrity": "sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.0.tgz", + "integrity": "sha512-Fq/7ziWKc6pYLYLIlCRaqJqEVTIZ5tZYfcW/mDK2AQ9v/sqjGFpj0On0/7hU50kbPVjLO4de+larPA1WwPZSfw==", "funding": [ { "type": "github", @@ -11108,18 +11132,17 @@ "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.3.0", + "avvio": "^8.2.1", "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": "^9.0.0", + "pino": "^8.17.0", "process-warning": "^3.0.0", "proxy-addr": "^2.0.7", "rfdc": "^1.3.0", @@ -11133,78 +11156,15 @@ "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.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "license": "ISC", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dependencies": { "reusify": "^1.0.4" } @@ -11267,14 +11227,13 @@ "license": "MIT" }, "node_modules/find-my-way": { - "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", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", - "safe-regex2": "^3.1.0" + "safe-regex2": "^2.0.0" }, "engines": { "node": ">=14" @@ -13421,22 +13380,15 @@ } }, "node_modules/light-my-request": { - "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", + "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==", "dependencies": { - "cookie": "^0.6.0", - "process-warning": "^3.0.0", + "cookie": "^0.5.0", + "process-warning": "^2.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", @@ -15261,11 +15213,10 @@ } }, "node_modules/picocolors": { - "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" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true }, "node_modules/picomatch": { "version": "3.0.1", @@ -15542,9 +15493,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -15560,11 +15511,10 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", - "source-map-js": "^1.2.1" + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -16390,12 +16340,11 @@ } }, "node_modules/ret": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", - "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", - "license": "MIT", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", "engines": { - "node": ">=10" + "node": ">=4" } }, "node_modules/retry": { @@ -16494,13 +16443,12 @@ } }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", + "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", "dev": true, - "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.5" }, "bin": { "rollup": "dist/bin/rollup" @@ -16510,22 +16458,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@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", + "@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", "fsevents": "~2.3.2" } }, @@ -16617,12 +16565,11 @@ } }, "node_modules/safe-regex2": { - "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", + "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==", "dependencies": { - "ret": "~0.4.0" + "ret": "~0.2.0" } }, "node_modules/safe-stable-stringify": { @@ -16991,11 +16938,10 @@ } }, "node_modules/source-map-js": { - "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==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -18865,15 +18811,14 @@ } }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz", + "integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==", "dev": true, - "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -18892,7 +18837,6 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", - "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -18910,9 +18854,6 @@ "sass": { "optional": true }, - "sass-embedded": { - "optional": true - }, "stylus": { "optional": true }, @@ -18970,14 +18911,13 @@ "dev": true }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -18987,14 +18927,13 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "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==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -19004,14 +18943,13 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -19021,14 +18959,13 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "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==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -19038,14 +18975,13 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -19055,14 +18991,13 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -19072,14 +19007,13 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "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==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -19089,14 +19023,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19106,14 +19039,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "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==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19123,14 +19055,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19140,14 +19071,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "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==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19157,14 +19087,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19174,14 +19103,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "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==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19191,14 +19119,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19208,14 +19135,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19225,14 +19151,13 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -19242,14 +19167,13 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -19259,14 +19183,13 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -19276,14 +19199,13 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "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==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" @@ -19293,14 +19215,13 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -19310,14 +19231,13 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "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==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -19327,14 +19247,13 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -19344,12 +19263,11 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -19357,29 +19275,29 @@ "node": ">=12" }, "optionalDependencies": { - "@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" + "@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" } }, "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 f64d7f858b..56784d314e 100644 --- a/backend/src/db/migrations/20240925100349_managed-secret-sharing.ts +++ b/backend/src/db/migrations/20240925100349_managed-secret-sharing.ts @@ -4,40 +4,27 @@ 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(); - if (!hasEncryptedSecret) { - t.binary("encryptedSecret").nullable(); - } + t.binary("encryptedSecret").nullable(); t.string("hashedHex").nullable().alter(); - if (!hasIdentifier) { - t.string("identifier", 64).nullable(); - t.unique("identifier"); - t.index("identifier"); - } + 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) => { - if (hasEncryptedSecret) { - t.dropColumn("encryptedSecret"); - } + t.dropColumn("encryptedSecret"); - if (hasIdentifier) { - t.dropColumn("identifier"); - } + 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 bdad443c9e..6b701eea4a 100644 --- a/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts +++ b/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts @@ -7,18 +7,15 @@ 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) => { - if (!hasProjectId) { - table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE"); - } + table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE"); - if (hasOrgId && hasSlug) { + if (hasOrgId) { table.unique(["orgId", "projectId", "slug"]); } @@ -33,7 +30,6 @@ 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) => { @@ -44,9 +40,7 @@ export async function down(knex: Knex): Promise { if (hasOrgId) { table.dropUnique(["orgId", "projectId", "slug"]); } - if (hasProjectId) { - table.dropColumn("projectId"); - } + 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 deleted file mode 100644 index 3b322b18f9..0000000000 --- a/backend/src/db/migrations/20241008172622_project-permission-split.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* 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 91a4402fb0..2e7fd60d3c 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,7 +4,6 @@ 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"; @@ -80,9 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) + permissions: JSON.stringify(packRules(permission)) }); return { privilege }; } @@ -162,9 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) + permissions: JSON.stringify(packRules(permission)) }); return { privilege }; } @@ -249,11 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F projectSlug: req.body.projectSlug, data: { ...updatedInfo, - permissions: permission - ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) - : undefined + permissions: permission ? JSON.stringify(packRules(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 9edd29030a..3920467572 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -3,10 +3,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; -import { - backfillPermissionV1SchemaToV2Schema, - ProjectPermissionV1Schema -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSchema } 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"; @@ -46,7 +43,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: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions) + permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.CREATE.permissions) }), response: { 200: z.object({ @@ -64,7 +61,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { projectSlug: req.params.projectSlug, data: { ...req.body, - permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) + permissions: JSON.stringify(packRules(req.body.permissions)) } }); return { role }; @@ -106,7 +103,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: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() }), response: { 200: z.object({ @@ -125,9 +122,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: req.params.roleId, data: { ...req.body, - permissions: req.body.permissions - ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) - : undefined + permissions: req.body.permissions ? JSON.stringify(packRules(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 1ba7e0e33a..7225caecf0 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,16 +1,13 @@ -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) => { @@ -34,9 +31,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }) .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: ProjectSpecificPrivilegePermissionSchema.describe( - PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions - ) + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) }), response: { 200: z.object({ @@ -54,17 +49,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, - permissions: JSON.stringify( - packRules( - backfillPermissionV1SchemaToV2Schema( - req.body.permissions.actions.map((action) => ({ - action, - subject: req.body.permissions.subject, - conditions: req.body.permissions.conditions - })) - ) - ) - ) + permissions: JSON.stringify(req.body.permissions) }); return { privilege }; } @@ -90,9 +75,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }) .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: ProjectSpecificPrivilegePermissionSchema.describe( - PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions - ), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), temporaryMode: z .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), @@ -121,17 +104,7 @@ 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( - packRules( - backfillPermissionV1SchemaToV2Schema( - req.body.permissions.actions.map((action) => ({ - action, - subject: req.body.permissions.subject, - conditions: req.body.permissions.conditions - })) - ) - ) - ) + permissions: JSON.stringify(req.body.permissions) }); return { privilege }; } @@ -158,9 +131,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr message: "Slug must be a valid slug" }) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), - permissions: ProjectSpecificPrivilegePermissionSchema.describe( - PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions - ).optional(), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) @@ -189,19 +160,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - 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, + permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : 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 deleted file mode 100644 index 980e4777b4..0000000000 --- a/backend/src/ee/routes/v2/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 86d64141d1..0000000000 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ /dev/null @@ -1,272 +0,0 @@ -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 220701410f..5695805073 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 new file mode 100644 index 0000000000..0e242f04c6 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts @@ -0,0 +1,36 @@ +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 f5151b0b7e..2633a026f6 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,6 +11,7 @@ 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, @@ -131,6 +132,22 @@ 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( { @@ -272,6 +289,22 @@ 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, @@ -282,6 +315,41 @@ 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 })); + } + 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 1e2c67f023..7c1f00a37c 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,6 +17,7 @@ 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"; @@ -77,6 +78,7 @@ export const accessApprovalRequestServiceFactory = ({ permissionService, accessApprovalRequestDAL, accessApprovalRequestReviewerDAL, + projectMembershipDAL, accessApprovalPolicyDAL, accessApprovalPolicyApproverDAL, additionalPrivilegeDAL, @@ -321,6 +323,22 @@ 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 77dc0d9d32..306c7ca26d 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,10 +4,7 @@ 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 { - ProjectPermissionDynamicSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, 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"; @@ -75,8 +72,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -148,8 +145,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -222,8 +219,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -287,8 +284,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -323,8 +320,8 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { 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 c202b3edaf..eec9094cb0 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -3,10 +3,7 @@ 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 { - ProjectPermissionDynamicSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, 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"; @@ -80,8 +77,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.CreateRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -149,8 +146,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.EditRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -228,8 +225,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.DeleteRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -285,12 +282,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.EditRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -335,8 +328,8 @@ export const dynamicSecretServiceFactory = ({ // verify user has access to each env in request environmentSlugs.forEach((environmentSlug) => ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ) ); } @@ -371,8 +364,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -417,8 +410,8 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -459,8 +452,8 @@ export const dynamicSecretServiceFactory = ({ // verify user has access to each env in request environmentSlugs.forEach((environmentSlug) => ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { 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 0e47b2160e..f5c1c857a4 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,6 +32,16 @@ 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>>[]) @@ -193,6 +203,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); return { ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) }; }; @@ -313,6 +324,7 @@ 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 ccd8967750..ea0289ab07 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 not found" }); } }) - .reduce((prev, curr) => prev.concat(curr), []); + .reduce((curr, prev) => prev.concat(curr), []); return createMongoAbility(rules, { conditionsMatcher @@ -98,7 +98,7 @@ export const permissionServiceFactory = ({ }); } }) - .reduce((prev, curr) => prev.concat(curr), []); + .reduce((curr, prev) => 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 8df85054df..60fcbec855 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().trim().min(1).array(), - [PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(), + [PermissionConditionOperators.$IN]: z.string().min(1).array(), + [PermissionConditionOperators.$ALL]: z.string().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 a3998206da..b2b34e488d 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,8 +1,9 @@ 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 { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { BadRequestError } from "@app/lib/errors"; import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types"; @@ -22,14 +23,6 @@ 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", @@ -45,8 +38,6 @@ 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", @@ -63,21 +54,22 @@ export enum ProjectPermissionSub { export type SecretSubjectFields = { environment: string; secretPath: string; - secretName?: string; - secretTags?: string[]; + // secretName: string; + // secretTags: string[]; }; -export type SecretFolderSubjectFields = { - environment: string; - secretPath: string; -}; - -export type DynamicSecretSubjectFields = { - environment: string; - secretPath: string; +export const CaslSecretsV2SubjectKnexMapper = (field: string) => { + switch (field) { + case "secretName": + return `${TableName.SecretV2}.key`; + case "secretTags": + return `${TableName.SecretTag}.slug`; + default: + break; + } }; -export type SecretImportSubjectFields = { +export type SecretFolderSubjectFields = { environment: string; secretPath: string; }; @@ -94,20 +86,6 @@ export type ProjectPermissionSet = | (ForcedSubject & SecretFolderSubjectFields) ) ] - | [ - ProjectPermissionDynamicSecretActions, - ( - | ProjectPermissionSub.DynamicSecrets - | (ForcedSubject & DynamicSecretSubjectFields) - ) - ] - | [ - ProjectPermissionActions, - ( - | ProjectPermissionSub.SecretImports - | (ForcedSubject & SecretImportSubjectFields) - ) - ] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Member] @@ -142,9 +120,7 @@ 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)); -// akhilmhdh: don't modify this for v2 -// if you want to update create a new schema -const SecretConditionV1Schema = z +const SecretConditionSchema = z .object({ environment: z.union([ z.string(), @@ -170,50 +146,16 @@ const SecretConditionV1Schema = z }) .partial(); -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 = [ +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() + }), z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -317,7 +259,7 @@ const GeneralPermissionSchema = [ ) }), 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." ) @@ -346,78 +288,18 @@ const GeneralPermissionSchema = [ "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( + subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).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 = () => { @@ -426,8 +308,6 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ ProjectPermissionSub.Secrets, - ProjectPermissionSub.SecretFolders, - ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, ProjectPermissionSub.SecretRotation, ProjectPermissionSub.Member, @@ -459,17 +339,6 @@ 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); @@ -501,34 +370,6 @@ 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); @@ -652,9 +493,6 @@ 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); @@ -757,52 +595,17 @@ export const isAtLeastAsPrivilegedWorkspace = ( }; /* eslint-enable */ -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 - ); +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}` }); + } }; 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 789c6d49a6..ea46e132cc 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,13 +1,11 @@ -import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, unpackRules } from "@casl/ability/extra"; +import { ForbiddenError } from "@casl/ability"; 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, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { ProjectUserAdditionalPrivilegeTemporaryMode, @@ -28,11 +26,6 @@ export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType< typeof projectUserAdditionalPrivilegeServiceFactory >; -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - export const projectUserAdditionalPrivilegeServiceFactory = ({ projectUserAdditionalPrivilegeDAL, projectMembershipDAL, @@ -74,10 +67,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ slug, permissions: customPermission }); - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; + return additionalPrivilege; } const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); @@ -92,10 +82,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; + return additionalPrivilege; }; const updateById = async ({ @@ -144,11 +131,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; + return additionalPrivilege; } const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { @@ -159,10 +142,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryRange: null, temporaryMode: null }); - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; + return additionalPrivilege; }; const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { @@ -185,10 +165,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; + return deletedPrivilege; }; const getPrivilegeDetailsById = async ({ @@ -216,10 +193,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); - return { - ...userPrivilege, - permissions: unpackPermissions(userPrivilege.permissions) - }; + return userPrivilege; }; const listPrivileges = async ({ @@ -245,10 +219,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ userId: projectMembership.userId, projectId: projectMembership.projectId }); - return userPrivileges.map((el) => ({ - ...el, - permissions: unpackPermissions(el.permissions) - })); + return userPrivileges; }; 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 bb77660aa6..e3526b0e35 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 0abe15858f..1908e75afe 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 } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import picomatch from "picomatch"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -344,8 +344,17 @@ export const secretApprovalPolicyServiceFactory = ({ environment, secretPath }: TGetBoardSapDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); - + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { secretPath, environment }) + ); 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 e321039b2b..bbc099956b 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, - getAllSecretReferences as getAllSecretReferencesV2Bridge + getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge } 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"; @@ -523,11 +523,11 @@ export const secretApprovalRequestServiceFactory = ({ skipMultilineEncoding: el.skipMultilineEncoding, key: el.key, references: el.encryptedValue - ? getAllSecretReferencesV2Bridge( + ? getAllNestedSecretReferencesV2Bridge( secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - ).nestedReferences + ) : [], type: SecretType.Shared })), @@ -547,11 +547,11 @@ export const secretApprovalRequestServiceFactory = ({ ? { encryptedValue: el.encryptedValue as Buffer, references: el.encryptedValue - ? getAllSecretReferencesV2Bridge( + ? getAllNestedSecretReferencesV2Bridge( secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - ).nestedReferences + ) : [] } : {}; @@ -1125,6 +1125,10 @@ 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) @@ -1288,23 +1292,6 @@ 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: "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 8f9237833d..8f74079f76 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -28,7 +28,8 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, - getAllSecretReferences + getAllNestedSecretReferences, + getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge } 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"; @@ -252,12 +253,11 @@ 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({ - secretImports: sourceSecretImports, + allowedImports: sourceSecretImports, secretDAL: secretV2BridgeDAL, folderDAL, secretImportDAL, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - hasSecretAccess: () => true + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }); // secrets that gets replicated across imports const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({ @@ -416,7 +416,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, - references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] + references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : [] }; }) }); @@ -442,7 +442,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue as Buffer, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, - references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] + references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : [] } }; }) @@ -687,7 +687,7 @@ export const secretReplicationServiceFactory = ({ secretCommentTag: doc.secretCommentTag, secretCommentCiphertext: doc.secretCommentCiphertext, skipMultilineEncoding: doc.skipMultilineEncoding, - references: getAllSecretReferences(doc.secretValue).nestedReferences + references: getAllNestedSecretReferences(doc.secretValue) }; }) }); @@ -723,7 +723,7 @@ export const secretReplicationServiceFactory = ({ secretCommentTag: doc.secretCommentTag, secretCommentCiphertext: doc.secretCommentCiphertext, skipMultilineEncoding: doc.skipMultilineEncoding, - references: getAllSecretReferences(doc.secretValue).nestedReferences + references: getAllNestedSecretReferences(doc.secretValue) } }; }) 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 1dfe276a3d..8408463ef5 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, TableName } from "@app/db/schemas"; +import { ProjectVersion } 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"; @@ -99,14 +99,13 @@ 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: { [`${TableName.SecretV2}.id` as "id"]: Object.values(outputs) } + $in: { id: Object.values(outputs) } }); if (selectedSecrets.length !== Object.values(outputs).length) throw new NotFoundError({ message: "Secrets not found" }); diff --git a/backend/src/lib/casl/knex.ts b/backend/src/lib/casl/knex.ts new file mode 100644 index 0000000000..cc9718fe09 --- /dev/null +++ b/backend/src/lib/casl/knex.ts @@ -0,0 +1,111 @@ +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 760317bad1..e7db061f30 100644 --- a/backend/src/lib/fn/array.ts +++ b/backend/src/lib/fn/array.ts @@ -81,25 +81,3 @@ 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 b8bc8ab57b..c336d7a9e9 100644 --- a/backend/src/lib/knex/dynamic.ts +++ b/backend/src/lib/knex/dynamic.ts @@ -2,31 +2,32 @@ import { Knex } from "knex"; import { UnauthorizedError } from "../errors"; -type TKnexDynamicPrimitiveOperator = { +type TKnexDynamicPrimitiveOperator = { operator: "eq" | "ne" | "startsWith" | "endsWith"; value: string; - field: Extract; + field: string; }; -type TKnexDynamicInOperator = { +type TKnexDynamicInOperator = { operator: "in"; value: string[] | number[]; - field: Extract; + field: string; }; -type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator; +type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator; -type TKnexGroupOperator = { +type TKnexGroupOperator = { operator: "and" | "or" | "not"; - value: (TKnexNonGroupOperator | TKnexGroupOperator)[]; + value: (TKnexNonGroupOperator | TKnexGroupOperator)[]; }; -export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator; +// 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 const buildDynamicKnexQuery = ( - rootQueryBuild: Knex.QueryBuilder, - dynamicQuery: TKnexDynamicOperator -) => { +export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => { const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }]; while (stack.length) { @@ -49,25 +50,34 @@ export const buildDynamicKnexQuery = ( break; } case "and": { - filterAst.value.forEach((el) => { - void queryBuilder.andWhere((subQueryBuilder) => { - buildDynamicKnexQuery(subQueryBuilder, el); + void queryBuilder.andWhere((subQueryBuilder) => { + filterAst.value.forEach((el) => { + stack.push({ + queryBuilder: subQueryBuilder, + filterAst: el + }); }); }); break; } case "or": { - filterAst.value.forEach((el) => { - void queryBuilder.orWhere((subQueryBuilder) => { - buildDynamicKnexQuery(subQueryBuilder, el); + void queryBuilder.orWhere((subQueryBuilder) => { + filterAst.value.forEach((el) => { + stack.push({ + queryBuilder: subQueryBuilder, + filterAst: el + }); }); }); break; } case "not": { - filterAst.value.forEach((el) => { - void queryBuilder.whereNot((subQueryBuilder) => { - buildDynamicKnexQuery(subQueryBuilder, el); + void queryBuilder.whereNot((subQueryBuilder) => { + filterAst.value.forEach((el) => { + stack.push({ + queryBuilder: subQueryBuilder, + filterAst: el + }); }); }); break; diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index f55d8e6e61..36d81ae349 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -3,7 +3,6 @@ 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"; @@ -21,10 +20,9 @@ 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, $complex, ...filter }: TFindFilter) => + ({ $in, $search, ...filter }: TFindFilter) => (bd: Knex.QueryBuilder) => { void bd.where(filter); if ($in) { @@ -41,9 +39,6 @@ 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 4113c0f2d0..76bfa9023a 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -61,7 +61,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} - ${JSON.stringify(error.subject)}` + message: `You are not allowed to ${error.action} on ${error.subjectType}` }); } 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 9144088791..326b283f50 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -5,7 +5,6 @@ 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"; @@ -1425,13 +1424,7 @@ export const registerRoutes = async ( }, { prefix: "/api/v1" } ); - await server.register( - async (v2Server) => { - await v2Server.register(registerV2EERoutes); - await v2Server.register(registerV2Routes); - }, - { prefix: "/api/v2" } - ); + await server.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 2bf170fc03..aa53ed50e7 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -9,10 +9,9 @@ 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 deleted file mode 100644 index 94c1dc57cc..0000000000 --- a/backend/src/server/routes/santizedSchemas/permission.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 36f0df591d..f8e02365a4 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -3,10 +3,7 @@ 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 { - ProjectPermissionDynamicSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionActions, 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"; @@ -195,15 +192,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { req.permission.orgId ); - const allowedDynamicSecretEnvironments = // filter envs user has access to + const permissiveEnvs = // filter envs user has access to environments.filter((environment) => permission.can( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ) ); - if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) { + if (includeDynamicSecrets && permissiveEnvs.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, @@ -212,7 +209,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, projectId, search, - environmentSlugs: allowedDynamicSecretEnvironments, + environmentSlugs: permissiveEnvs, path: secretPath, isInternal: true }); @@ -227,7 +224,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, orderBy, orderDirection, - environmentSlugs: allowedDynamicSecretEnvironments, + environmentSlugs: permissiveEnvs, path: secretPath, limit: remainingLimit, offset: adjustedOffset, @@ -244,13 +241,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } - if (includeSecrets) { + if (includeSecrets && permissiveEnvs.length) { // 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, + environments: permissiveEnvs, actorAuthMethod: req.permission.authMethod, projectId, path: secretPath, @@ -263,7 +260,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, - environments, + environments: permissiveEnvs, actorAuthMethod: req.permission.authMethod, projectId, path: secretPath, @@ -275,7 +272,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { isInternal: true }); - for await (const environment of environments) { + for await (const environment of permissiveEnvs) { 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 934c1c8e1e..6d996022a5 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, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; +import { fnSecretBulkInsert, getAllNestedSecretReferences } 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 = getAllSecretReferences(el.secretValue).nestedReferences; + const references = getAllNestedSecretReferences(el.secretValue); 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 7002e12291..7cf77cb26c 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -67,8 +67,7 @@ const getIntegrationSecretsV2 = async ( folderDAL, secretDAL: secretV2BridgeDAL, secretImportDAL, - secretImports, - hasSecretAccess: () => true + allowedImports: secretImports }); 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 d511f96890..47a92c3844 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -89,10 +89,7 @@ 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); @@ -165,10 +162,7 @@ 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 e9c8cb1da9..49c4f81c9a 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -2,6 +2,7 @@ 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, @@ -9,7 +10,6 @@ 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 new file mode 100644 index 0000000000..c8f7d885e5 --- /dev/null +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -0,0 +1,6 @@ +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 6815aaa111..b16d90b6b0 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -12,6 +12,7 @@ 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, @@ -59,10 +60,20 @@ export const secretFolderServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.SecretFolders, { environment, 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.Create, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new NotFoundError({ message: "Environment not found", name: "Create folder" }); @@ -150,10 +161,20 @@ export const secretFolderServiceFactory = ({ ); folders.forEach(({ environment, path: secretPath }) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.SecretFolders, { environment, 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 }) + ); + } }); const result = await folderDAL.transaction(async (tx) => @@ -246,10 +267,20 @@ export const secretFolderServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.SecretFolders, { environment, 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 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 }) + ); + } const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!parentFolder) throw new NotFoundError({ message: "Secret path not found" }); @@ -320,10 +351,20 @@ export const secretFolderServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.SecretFolders, { environment, 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 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 }) + ); + } const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new NotFoundError({ message: "Environment not found", name: "Create folder" }); diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d75a255140..b21a6c3ced 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -27,7 +27,6 @@ type TSecretImportSecretsV2 = { slug: string; name: string; }; - id: string; folderId: string | undefined; importFolderId: string; secrets: (TSecretsV2 & { @@ -140,22 +139,24 @@ export const fnSecretsFromImports = async ({ return secrets; }; -/* eslint-disable no-await-in-loop, no-continue */ export const fnSecretsV2FromImports = async ({ - secretImports: rootSecretImports, + allowedImports: possibleCyclicImports, folderDAL, secretDAL, secretImportDAL, + depth = 0, + cyclicDetector = new Set(), decryptor, - expandSecretReferences, - hasSecretAccess + expandSecretReferences }: { - secretImports: (Omit & { + allowedImports: (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; @@ -163,107 +164,92 @@ export const fnSecretsV2FromImports = async ({ secretPath: string; environment: string; }) => Promise; - hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { - const cyclicDetector = new Set(); - const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ - { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } - ]; - - const processedImports: TSecretImportSecretsV2[] = []; - - 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)) - ); + // avoid going more than a depth + if (depth >= LEVEL_BREAK) return []; - if (!sanitizedImports.length) continue; + const allowedImports = possibleCyclicImports.filter( + ({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath)) + ); - const importedFolders = await folderDAL.findByManySecretPath( - sanitizedImports.map(({ importEnv, importPath }) => ({ + const importedFolders = ( + await folderDAL.findByManySecretPath( + allowedImports.map(({ importEnv, importPath }) => ({ envId: importEnv.id, secretPath: importPath })) - ); - if (!importedFolders.length) continue; + ) + ).filter(Boolean); // remove undefined ones + if (!importedFolders.length) { + return []; + } - const importedFolderIds = importedFolders.map((el) => el?.id) as string[]; - const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`); + 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 importedSecrets = await secretDAL.find( - { - $in: { folderId: importedFolderIds }, - type: SecretType.Shared - }, - { - sort: [["id", "asc"]] - } - ); - const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); + const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); - sanitizedImports.forEach(({ importPath, importEnv }) => { - cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath)); + 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 }); - // 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 secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId); - 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. - })); + 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); - if (deeperImportsGroupByFolderId?.[sourceImportFolder?.id || ""]) { - stack.push({ - secretImports: deeperImportsGroupByFolderId[sourceImportFolder?.id || ""], - depth: depth + 1, - parentImportedSecrets: secretsWithDuplicate - }); - } + return { + secretPath: importPath, + environment: importEnv.slug, + environmentInfo: importEnv, + folderId: importedFolders?.[i]?.id, + id, + importFolderId: folderId, + secrets: unique(secretsWithDuplicate, (el) => el.secretKey) + }; + }); - if (isFirstIteration) { - processedImports.push({ - secretPath: importPath, - environment: importEnv.slug, - environmentInfo: importEnv, - folderId: importedFolders?.[i]?.id, - id, - importFolderId: folderId, - secrets: secretsWithDuplicate - }); - } else { - parentImportedSecrets.push(...secretsWithDuplicate); - } - }); - } - /* eslint-enable */ if (expandSecretReferences) { await Promise.allSettled( - processedImports.map((processedImport) => { - // eslint-disable-next-line - processedImport.secrets = unique(processedImport.secrets, (i) => i.key); - return Promise.allSettled( + processedImports.map((processedImport) => + Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, @@ -274,8 +260,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 e1d0291f64..5551b01805 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.SecretImports, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -191,7 +191,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -277,7 +277,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -342,8 +342,8 @@ export const secretImportServiceFactory = ({ // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -366,7 +366,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -414,7 +414,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -446,7 +446,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -489,7 +489,7 @@ export const secretImportServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { + subject(ProjectPermissionSub.Secrets, { environment: folder.environment.envSlug, secretPath: folderWithPath.path }) @@ -532,19 +532,20 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { 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((el) => + + const allowedImports = secretImports.filter(({ importEnv, importPath }) => permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { - environment: el.importEnv.slug, - secretPath: el.importPath + environment: importEnv.slug, + secretPath: importPath }) ) ); @@ -569,7 +570,7 @@ export const secretImportServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) return []; @@ -577,6 +578,16 @@ 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({ @@ -584,21 +595,11 @@ export const secretImportServiceFactory = ({ projectId }); const importedSecrets = await fnSecretsV2FromImports({ - secretImports, + allowedImports, folderDAL, secretDAL: secretV2BridgeDAL, secretImportDAL, - 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 - }) - ) + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }); return importedSecrets; } @@ -609,21 +610,7 @@ export const secretImportServiceFactory = ({ name: "bot_not_found_error" }); - 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 - }); + 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 adbd935eea..1a9397e91a 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,14 +4,7 @@ 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 { - buildFindFilter, - ormify, - selectAllTableCols, - sqlNestRelationships, - TFindFilter, - TFindOpt -} from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -20,97 +13,6 @@ 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) @@ -582,8 +484,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - countByFolderIds, - findOne, - find + countByFolderIds }; }; 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 13ae7ebba2..1ae7ce6dc2 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,10 +30,9 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3; * // { environment: 'prod', secretPath: '/anotherFolder' } * // ] */ -export const getAllSecretReferences = (maybeSecretReference: string) => { +export const getAllNestedSecretReferences = (maybeSecretReference: string) => { const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]); - - const nestedReferences = references + return references .filter((el) => el.includes(".")) .map((el) => { const [environment, ...secretPathList] = el.split("."); @@ -43,8 +42,6 @@ export const getAllSecretReferences = (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 @@ -328,13 +325,16 @@ type TRecursivelyFetchSecretsFromFoldersArg = { projectId: string; environment: string; currentPath: string; + hasAccess: (environment: string, secretPath: string) => boolean; }; export const recursivelyGetSecretPaths = async ({ folderDAL, projectEnvDAL, projectId, - environment + environment, + currentPath, + hasAccess }: TRecursivelyFetchSecretsFromFoldersArg) => { const env = await projectEnvDAL.findOne({ projectId, @@ -360,7 +360,12 @@ export const recursivelyGetSecretPaths = async ({ folderId: p.folderId })); - return paths; + // 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; }; // used to convert multi line ones to quotes ones with \n const formatMultiValueEnv = (val?: string) => { @@ -374,7 +379,7 @@ type TInterpolateSecretArg = { decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined; secretDAL: Pick; folderDAL: Pick; - canExpandValue: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; + canExpandValue: (environment: string, secretPath: string) => boolean; }; const MAX_SECRET_REFERENCE_DEPTH = 10; @@ -385,29 +390,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] || { value: "", tags: [] }; + return secretCache[cacheKey][secretKey] || ""; } const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) return { value: "", tags: [] }; + if (!folder) return ""; 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] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) }; + prev[secret.key] = decryptSecret(secret.encryptedValue) || ""; return prev; }, {}); secretCache[cacheKey] = decryptedSecret; - return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; + return secretCache[cacheKey][secretKey] || ""; }; const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => { @@ -433,43 +438,43 @@ export const expandSecretReferencesFactory = ({ if (entities.length === 1) { const [secretKey] = entities; - // eslint-disable-next-line no-continue,no-await-in-loop - const referredValue = await fetchSecret(environment, secretPath, secretKey); - if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) + if (!canExpandValue(environment, secretPath)) 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] = referredValue; - if (INTERPOLATION_SYNTAX_REG.test(referredValue.value)) { + secretCache[cacheKey][secretKey] = referedValue; + if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { stack.push({ - value: referredValue.value, + value: referedValue, secretPath, environment, depth: depth + 1 }); } - if (referredValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referredValue.value); + if (referedValue) { + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); } } else { const secretReferenceEnvironment = entities[0]; const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); const secretReferenceKey = entities[entities.length - 1]; - // eslint-disable-next-line no-await-in-loop - const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); - if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) + if (!canExpandValue(secretReferenceEnvironment, secretReferencePath)) 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.value)) { + if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { stack.push({ - value: referedValue.value, + value: referedValue, secretPath: secretReferencePath, environment: secretReferenceEnvironment, depth: depth + 1 @@ -477,7 +482,7 @@ export const expandSecretReferencesFactory = ({ } if (referedValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value); + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); } } } 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 380bbec1f0..0d2797800a 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,7 +1,6 @@ -import { ForbiddenError, PureAbility, subject } from "@casl/ability"; -import { z } from "zod"; +import { ForbiddenError, subject } from "@casl/ability"; -import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas"; +import { ProjectMembershipRole, SecretsV2Schema, SecretType } 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"; @@ -9,7 +8,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 { diff, groupBy } from "@app/lib/fn"; +import { groupBy } from "@app/lib/fn"; import { setKnexStringValue } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -29,7 +28,7 @@ import { fnSecretBulkDelete, fnSecretBulkInsert, fnSecretBulkUpdate, - getAllSecretReferences, + getAllNestedSecretReferences, recursivelyGetSecretPaths, reshapeBridgeSecret } from "./secret-v2-bridge-fns"; @@ -44,7 +43,6 @@ import { TGetSecretsDTO, TGetSecretVersionsDTO, TMoveSecretsDTO, - TSecretReference, TUpdateManySecretDTO, TUpdateSecretDTO } from "./secret-v2-bridge-types"; @@ -58,7 +56,7 @@ type TSecretV2BridgeServiceFactoryDep = { secretVersionTagDAL: Pick; secretTagDAL: TSecretTagDALFactory; permissionService: Pick; - projectEnvDAL: Pick; + projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" @@ -95,83 +93,6 @@ 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, @@ -189,6 +110,10 @@ 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) @@ -209,7 +134,6 @@ 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({ @@ -220,31 +144,24 @@ export const secretV2BridgeServiceFactory = ({ // 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(",")}` }); - - 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); + if ((inputSecret.tagIds || []).length !== tags.length) throw new NotFoundError({ message: "Tag not found" }); + 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, @@ -252,20 +169,20 @@ export const secretV2BridgeServiceFactory = ({ { version: 1, type, - reminderRepeatDays: inputSecretData.secretReminderRepeatDays, + reminderRepeatDays: el.secretReminderRepeatDays, encryptedComment: setKnexStringValue( - inputSecretData.secretComment, + el.secretComment, (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob ), - encryptedValue: inputSecretData.secretValue - ? secretManagerEncryptor({ plainText: Buffer.from(inputSecretData.secretValue) }).cipherTextBlob + encryptedValue: el.secretValue + ? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob : undefined, - reminderNote: inputSecretData.secretReminderNote, - skipMultilineEncoding: inputSecretData.skipMultilineEncoding, + reminderNote: el.secretReminderNote, + skipMultilineEncoding: el.skipMultilineEncoding, key: secretName, userId: inputSecret.type === SecretType.Personal ? actorId : null, tagIds: inputSecret.tagIds, - references: nestedReferences + references } ], secretDAL, @@ -311,6 +228,10 @@ 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" }); @@ -355,33 +276,6 @@ 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, @@ -389,36 +283,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: "Tag not found" }); + const { secretName, secretValue } = inputSecret; const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - 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 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 updatedSecret = await secretDAL.transaction(async (tx) => @@ -492,6 +386,10 @@ 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) @@ -516,15 +414,6 @@ export const secretV2BridgeServiceFactory = ({ }) }); 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({ @@ -593,7 +482,13 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + // verify user has access to all environments + environments.forEach((environment) => + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ) + ); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -639,7 +534,10 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -664,15 +562,22 @@ export const secretV2BridgeServiceFactory = ({ environments: string[]; isInternal?: boolean; }) => { - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId - ); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + 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 }) + ) + ); } let paths: { folderId: string; path: string; environment: string }[] = []; @@ -699,34 +604,22 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - 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) - }) - ) + 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() + : "" + } ) - .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; }; @@ -752,8 +645,6 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); - let paths: { folderId: string; path: string }[] = []; if (recursive) { @@ -762,13 +653,26 @@ export const secretV2BridgeServiceFactory = ({ projectEnvDAL, projectId, environment, - currentPath: path + currentPath: path, + hasAccess: (permissionEnvironment, permissionSecretPath) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: permissionEnvironment, + secretPath: permissionSecretPath + }) + ) }); 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: [] }; @@ -789,44 +693,27 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - 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 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 expandSecretReferences = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + canExpandValue: (expandEnvironment, expandSecretPath) => permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) + subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) ) }); @@ -857,24 +744,26 @@ export const secretV2BridgeServiceFactory = ({ } const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); - const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); + 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 importedSecrets = await fnSecretsV2FromImports({ - secretImports: allowedImports, + allowedImports, secretDAL, folderDAL, secretImportDAL, expandSecretReferences, - 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 - }) - ) + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }); return { @@ -904,7 +793,10 @@ 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({ @@ -940,43 +832,17 @@ export const secretV2BridgeServiceFactory = ({ userId: secretType === SecretType.Personal ? actorId : null, key: secretName }) - .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) - }) - ); + .then((el) => SecretsV2Schema.parse({ ...el, id: el.secretId }))); const expandSecretReferences = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => + canExpandValue: (expandEnvironment, expandSecretPath) => permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) + subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) ) }); @@ -985,23 +851,25 @@ 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({ - secretImports, + allowedImports, secretDAL, folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { @@ -1059,6 +927,10 @@ 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) @@ -1068,68 +940,20 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secrets = await secretDAL.find({ + const secrets = await secretDAL.findBySecretKeys( folderId, - $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 - } - ] - })) - } - ] - } - }); + inputSecrets.map((el) => ({ + key: el.secretKey, + type: 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: `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); + if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); @@ -1137,7 +961,16 @@ export const secretV2BridgeServiceFactory = ({ const newSecrets = await secretDAL.transaction(async (tx) => fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { - const references = secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences; + const references = getAllNestedSecretReferences(el.secretValue); + references.forEach((referredSecret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: referredSecret.environment, + secretPath: referredSecret.secretPath + }) + ); + }); return { version: 1, @@ -1199,6 +1032,10 @@ 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) @@ -1208,128 +1045,38 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secretsToUpdate = await secretDAL.find({ + const secretsToUpdate = await secretDAL.findBySecretKeys( folderId, - $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 - } - ] - })) - } - ] - } - }); + inputSecrets.map((el) => ({ + key: el.secretKey, + type: 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.find({ + const secrets = await secretDAL.findBySecretKeys( folderId, - $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 - } - ] - })) - } - ] - } - }); + secretsWithNewName.map((el) => ({ + key: el.newSecretName as string, + type: SecretType.Shared + })) + ); if (secrets.length) throw new BadRequestError({ message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}` }); - - 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); + + // 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 { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); @@ -1344,10 +1091,22 @@ export const secretV2BridgeServiceFactory = ({ typeof el.secretValue !== "undefined" ? { encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, - references: secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences + references: getAllNestedSecretReferences(el.secretValue) } : {}; + 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: { @@ -1405,6 +1164,10 @@ 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) @@ -1414,47 +1177,17 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secretsToDelete = await secretDAL.find({ + const secretsToDelete = await secretDAL.findBySecretKeys( folderId, - $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 - } - ] - })) - } - ] - } - }); + inputSecrets.map((el) => ({ + key: el.secretKey, + type: 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({ @@ -1563,8 +1296,7 @@ export const secretV2BridgeServiceFactory = ({ .map(({ id, encryptedValue }) => ({ secretId: id, references: encryptedValue - ? getAllSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString()) - .nestedReferences + ? getAllNestedSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString()) : [] })), tx @@ -1595,6 +1327,21 @@ 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({ @@ -1617,20 +1364,9 @@ export const secretV2BridgeServiceFactory = ({ const sourceSecrets = await secretDAL.find({ type: SecretType.Shared, $in: { - [`${TableName.SecretV2}.id` as "id"]: secretIds + 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({ @@ -1701,32 +1437,6 @@ 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, @@ -1793,7 +1503,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, reminderRepeatDays: doc.reminderRepeatDays, - references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] + references: doc.value ? getAllNestedSecretReferences(doc.value) : [] }; }) }); @@ -1822,7 +1532,7 @@ export const secretV2BridgeServiceFactory = ({ ...(doc.encryptedValue ? { encryptedValue: doc.encryptedValue, - references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] + references: doc.value ? getAllNestedSecretReferences(doc.value) : [] } : { 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 0bb93198de..a76c57561e 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,12 +15,6 @@ 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 4b17bdf921..1d0b89b467 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, - getAllSecretReferences + getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; @@ -791,7 +791,7 @@ export const createManySecretsRawFnFactory = ({ : null, skipMultilineEncoding: secret.skipMultilineEncoding, tags: secret.tags, - references: getAllSecretReferences(secret.secretValue).nestedReferences + references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) }; }); @@ -971,7 +971,7 @@ export const updateManySecretsRawFnFactory = ({ : null, skipMultilineEncoding: secret.skipMultilineEncoding, tags: secret.tags, - references: getAllSecretReferences(secret.secretValue).nestedReferences + references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) }; }); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 3b949f5536..4076b179fb 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, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; +import { expandSecretReferencesFactory, getAllNestedSecretReferences } 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,8 +342,7 @@ export const secretQueueFactory = ({ secretDAL: secretV2BridgeDAL, expandSecretReferences, secretImportDAL, - secretImports, - hasSecretAccess: () => true + allowedImports: secretImports }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { @@ -1148,7 +1147,7 @@ export const secretQueueFactory = ({ : ""; const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob; // create references - const references = getAllSecretReferences(value).nestedReferences; + const references = getAllNestedSecretReferences(value); 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 0b0f627efe..9d3037ec0f 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -2407,26 +2407,17 @@ 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 deleted file mode 100644 index b65b0c3074..0000000000 --- a/frontend/src/components/permissions/GlobPermissionInfo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 4fc5acde95..f1af141f2c 100644 --- a/frontend/src/components/permissions/ProjectPermissionCan.tsx +++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx @@ -1,25 +1,23 @@ import { FunctionComponent, ReactNode } from "react"; -import { AbilityTuple, MongoAbility } from "@casl/ability"; -import { Can } from "@casl/react"; +import { BoundCanProps, Can } from "@casl/react"; -import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext"; +import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext"; -import { Tooltip } from "../v2/Tooltip"; +import { Tooltip } from "../v2"; -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; - children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode); - passThrough?: boolean; - I: T[0]; - a: T[1]; - ability?: MongoAbility; -}; + // 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; -export const ProjectPermissionCan: FunctionComponent> = ({ +export const ProjectPermissionCan: FunctionComponent = ({ label = "Access restricted", children, passThrough = true, @@ -33,7 +31,9 @@ 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 any) : children; + typeof children === "function" + ? children(isAllowed, ability as TProjectPermission) + : children; if (!isAllowed && passThrough) { return {finalChild}; diff --git a/frontend/src/components/permissions/index.tsx b/frontend/src/components/permissions/index.tsx index 5103b0f73f..8d523c3111 100644 --- a/frontend/src/components/permissions/index.tsx +++ b/frontend/src/components/permissions/index.tsx @@ -1,4 +1,3 @@ -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 ce3f03d01c..dcc7da62c2 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -12,7 +12,6 @@ type Props = { placeholder?: string; className?: string; dropdownContainerClassName?: string; - containerClassName?: string; isLoading?: boolean; position?: "item-aligned" | "popper"; isDisabled?: boolean; @@ -32,13 +31,12 @@ 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 4b04f5cb84..afad2cd86a 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -3,6 +3,5 @@ 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 307ef74af3..d47a3d01b4 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -7,14 +7,6 @@ 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", @@ -29,7 +21,7 @@ export enum PermissionConditionOperators { $ALL = "$all", $REGEX = "$regex", $EQ = "$eq", - $NEQ = "$ne", + $NEQ = "$neq", $GLOB = "$glob" } @@ -45,7 +37,7 @@ export type TPermissionConditionOperators = { export type TPermissionCondition = Record< string, | string - | { $in: string[]; $all: string[]; $regex: string; $eq: string; $ne: string; $glob: string } + | { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string } >; export enum ProjectPermissionSub { @@ -60,11 +52,9 @@ export enum ProjectPermissionSub { Tags = "tags", AuditLogs = "audit-logs", IpAllowList = "ip-allowlist", - Project = "workspace", + Workspace = "workspace", Secrets = "secrets", SecretFolders = "secret-folders", - SecretImports = "secret-imports", - DynamicSecrets = "dynamic-secrets", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", @@ -78,24 +68,7 @@ export enum ProjectPermissionSub { Cmek = "cmek" } -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 = { +type SubjectFields = { environment: string; secretPath: string; }; @@ -103,30 +76,13 @@ export type SecretImportSubjectFields = { export type ProjectPermissionSet = | [ ProjectPermissionActions, - ( - | ProjectPermissionSub.Secrets - | (ForcedSubject & SecretSubjectFields) - ) + ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) ] | [ ProjectPermissionActions, ( | ProjectPermissionSub.SecretFolders - | (ForcedSubject & SecretFolderSubjectFields) - ) - ] - | [ - ProjectPermissionDynamicSecretActions, - ( - | ProjectPermissionSub.DynamicSecrets - | (ForcedSubject & DynamicSecretSubjectFields) - ) - ] - | [ - ProjectPermissionActions, - ( - | ProjectPermissionSub.SecretImports - | (ForcedSubject & SecretImportSubjectFields) + | (ForcedSubject & SubjectFields) ) ] | [ProjectPermissionActions, ProjectPermissionSub.Role] @@ -139,19 +95,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.Project] - | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] + | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] - | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] - | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; + | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]; + export type TProjectPermission = MongoAbility; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 91dae5d2d2..ada4368330 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -11,7 +11,6 @@ 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 22c91ab52e..103ff61b7b 100644 --- a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx +++ b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx @@ -1,29 +1,31 @@ import { ComponentType } from "react"; -import { AbilityTuple } from "@casl/ability"; +import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability"; import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { useProjectPermission } from "@app/context"; -import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { TProjectPermission, useProjectPermission } from "@app/context"; -type Props = { - className?: string; - containerClassName?: string; - action: T[0]; - subject: T[1]; -}; +type Props = (T extends AbilityTuple + ? { + action: T[0]; + subject: Extract; + } + : { + action: string; + subject: string; + }) & { className?: string; containerClassName?: string }; -export const withProjectPermission = ( - Component: ComponentType, "action" | "subject"> & T>, - { action, subject, className, containerClassName }: Props +export const withProjectPermission = ( + Component: ComponentType, + { action, subject, className, containerClassName }: Props["abilities"]> ) => { - const HOC = (hocProps: Omit, "action" | "subject"> & T) => { + const HOC = (hocProps: 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 as any)) { + if (permission.cannot(action as any, subject)) { return (
["dashboard"] as const, @@ -155,20 +154,10 @@ 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, - totalUniqueSecretsInPage: uniqueSecrets.length, - totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length, - totalUniqueFoldersInPage: uniqueFolders.length + secrets: secrets ? mergePersonalSecrets(secrets) : undefined }; }, []), keepPreviousData: true diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 865d8541de..08e75ee3b5 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -12,9 +12,6 @@ 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 2ee6519f67..fb21a425e0 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx @@ -1,3 +1,4 @@ +import { packRules } from "@casl/ability/extra"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -15,7 +16,10 @@ export const useCreateProjectUserAdditionalPrivilege = () => { return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({ mutationFn: async (dto) => { - const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", dto); + const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", { + ...dto, + permissions: packRules(dto.permissions) + }); return data.privilege; }, onSuccess: (_, { projectMembershipId }) => { @@ -31,7 +35,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => { mutationFn: async (dto) => { const { data } = await apiRequest.patch( `/api/v1/additional-privilege/users/${dto.privilegeId}`, - dto + { ...dto, permissions: dto.permissions ? packRules(dto.permissions) : undefined } ); return data.privilege; }, diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx index 261821c1fe..41c9a0dcc2 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx @@ -1,3 +1,4 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -17,7 +18,10 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => { } = await apiRequest.get<{ privilege: Omit & { permissions: unknown }; }>(`/api/v1/additional-privilege/users/${privilegeId}`); - return privilege; + return { + ...privilege, + permissions: unpackRules(privilege.permissions as PackRule[]) + }; }; export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => { @@ -40,7 +44,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => { }>("/api/v1/additional-privilege/users", { params: { projectMembershipId } }); return privileges.map((el) => ({ ...el, - permissions: el.permissions as TProjectPermission[] + permissions: unpackRules(el.permissions as PackRule[]) })); } }); diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx index da40005c23..b757a07ab4 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx @@ -4,15 +4,6 @@ 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; @@ -21,21 +12,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; @@ -44,7 +35,7 @@ export type TCreateProjectUserPrivilegeDTO = { temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode; temporaryRange?: string; temporaryAccessStartTime?: string; - permissions: TProjectSpecificPrivilegePermission; + permissions: TProjectPermission[]; }; export type TUpdateProjectUserPrivlegeDTO = { diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 1c6b85b667..3f782171e1 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/v2/workspace/${projectSlug}/roles`, dto); + } = await apiRequest.post(`/api/v1/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/v2/workspace/${projectSlug}/roles/${id}`, dto); + } = await apiRequest.patch(`/api/v1/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/v2/workspace/${projectSlug}/roles/${id}`); + } = await apiRequest.delete(`/api/v1/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 1726abfd3e..77e82e4fae 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -7,8 +7,6 @@ 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 { @@ -51,7 +49,7 @@ export const roleQueryKeys = { export const getProjectRoles = async (projectId: string) => { const { data } = await apiRequest.get<{ roles: Array> }>( - `/api/v2/workspace/${projectId}/roles` + `/api/v1/workspace/${projectId}/roles` ); return data.roles; }; @@ -68,7 +66,7 @@ export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) = queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug), queryFn: async () => { const { data } = await apiRequest.get<{ role: TProjectRole }>( - `/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}` + `/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}` ); return data.role; }, @@ -136,7 +134,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss permissions: PackRule>>[]; membership: Omit & { roles: { role: string }[] }; }; - }>(`/api/v2/workspace/${workspaceId}/permissions`, {}); + }>(`/api/v1/workspace/${workspaceId}/permissions`, {}); return data.data; }; @@ -148,32 +146,8 @@ export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPer enabled: Boolean(workspaceId), select: (data) => { const rule = unpackRules>>(data.permissions); - 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 ability = createMongoAbility(rule, { conditionsMatcher }); + 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 50cdf5a220..da28d9ca8e 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -40,7 +40,6 @@ 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 eef80a892a..b923906972 100644 --- a/frontend/src/lib/fn/array.ts +++ b/frontend/src/lib/fn/array.ts @@ -13,22 +13,3 @@ 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 deleted file mode 100644 index 0af1ead2dd..0000000000 --- a/frontend/src/lib/fn/object.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * 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 51581de4d9..7894d0e785 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: { environment: string; secretPath?: { $glob: string } } = { - environment: data.environmentSlug - }; + const conditions: Record = { environment: data.environmentSlug }; if (data.secretPath) { conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) }; } await updateUserPrivilege.mutateAsync({ privilegeId: privilege.id, ...data.temporaryAccess, - permissions: { - subject: ProjectPermissionSub.Secrets, - conditions, - actions: actions.filter((i) => i.allowed).map((i) => i.action) - }, + permissions: actions + .filter(({ allowed }) => allowed) + .map(({ action }) => ({ + action, + subject: [ProjectPermissionSub.Secrets], + conditions + })), projectMembershipId: privilege.projectMembershipId }); createNotification({ @@ -642,13 +642,15 @@ export const SpecificPrivilegeSection = ({ membershipId }: Props) => { if (createUserPrivilege.isLoading) return; try { await createUserPrivilege.mutateAsync({ - permissions: { - actions: [ProjectPermissionActions.Read], - subject: ProjectPermissionSub.Secrets, - conditions: { - environment: currentWorkspace?.environments?.[0].slug || "" + permissions: [ + { + action: 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 5832e5a14d..7dd9816910 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts @@ -7,7 +7,6 @@ import { } from "@app/context"; import { PermissionConditionOperators, - ProjectPermissionDynamicSecretActions, TPermissionCondition, TPermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; @@ -29,12 +28,8 @@ const CmekPolicyActionSchema = z.object({ decrypt: 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 SecretFolderPolicyActionSchema = z.object({ + read: z.boolean().optional() }); const SecretRollbackPolicyActionSchema = z.object({ @@ -47,29 +42,11 @@ const WorkspacePolicyActionSchema = z.object({ delete: z.boolean().optional() }); -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" } - ); +const ConditionSchema = z.object({ + operator: z.string(), + lhs: z.string(), + rhs: z.string().min(1) +}); export const formSchema = z.object({ name: z.string().trim(), @@ -82,29 +59,27 @@ export const formSchema = z.object({ permissions: z .object({ [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ - 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 + 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" } + ) }) .array() .default([]), + [ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]), @@ -123,7 +98,7 @@ export const formSchema = z.object({ [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]), - [ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]), + [ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]), [ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]), @@ -135,22 +110,8 @@ 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({ @@ -177,15 +138,12 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const formVal: Partial = {}; permissions.forEach((permission) => { - const { subject: caslSub, action, conditions, inverted } = permission; + const { subject: caslSub, action, conditions } = 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, @@ -208,67 +166,37 @@ 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 (isConditionalSubjects(subject)) { + if (subject === ProjectPermissionSub.Secrets) { if (!formVal[subject]) formVal[subject] = []; - - 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 - }); - } + formVal[subject]!.push({ + read: canRead, + create: canCreate, + edit: canEdit, + delete: canDelete, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [] + }); } 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.Project) { + } else if (subject === ProjectPermissionSub.Workspace) { 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.Project]![0].edit = true; + if (canEdit) formVal[subject as ProjectPermissionSub.Workspace]![0].edit = true; if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true; } else if (subject === ProjectPermissionSub.SecretRollback) { const canRead = action.includes(ProjectPermissionActions.Read); @@ -278,6 +206,12 @@ 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); @@ -330,7 +264,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 !== "inverted" + (el) => actions?.[el as keyof typeof actions] && el !== "conditions" ); const caslConditions = "conditions" in actions @@ -340,7 +274,6 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { permissions.push({ action: caslActions, subject, - inverted: (actions as { inverted?: boolean })?.inverted, conditions: caslConditions }); }); @@ -355,7 +288,7 @@ export type TProjectPermissionObject = { label: string; value: keyof Omit< NonNullable[K]>[number], - "conditions" | "inverted" + "conditions" >; }[]; }; @@ -373,42 +306,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { }, [ProjectPermissionSub.SecretFolders]: { title: "Secret Folders", - 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 } - ] + actions: [{ label: "Read Only", value: "read" }] }, [ProjectPermissionSub.Cmek]: { title: "KMS", @@ -434,7 +332,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, - [ProjectPermissionSub.Project]: { + [ProjectPermissionSub.Workspace]: { 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 a7ac060104..fb35678973 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -10,15 +10,13 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; -import { GeneralPermissionConditions } from "./components/GeneralPermissionConditions"; -import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies"; +import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions"; import { NewPermissionRule } from "./components/NewPermissionRule"; import { SecretPermissionConditions } from "./components/SecretPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; import { formRolePermission2API, formSchema, - isConditionalSubjects, PROJECT_PERMISSION_OBJECT, rolePermission2Form, TFormSchema @@ -29,17 +27,6 @@ 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); @@ -143,15 +130,17 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
{!isLoading && } {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( - - {renderConditionalComponents(subject, isDisabled)} - + {subject === ProjectPermissionSub.Secrets ? ( + + ) : undefined} + ))}
diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx deleted file mode 100644 index 2ac518038f..0000000000 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx +++ /dev/null @@ -1,176 +0,0 @@ -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/GeneralPermissionPolicies.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx similarity index 66% rename from frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx rename to frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx index 58c403579a..cdf0f5151c 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx @@ -1,24 +1,14 @@ import { cloneElement } from "react"; import { Controller, useFieldArray, useFormContext } from "react-hook-form"; -import { - faChevronDown, - faChevronRight, - faInfoCircle, - faPlus, - faTrash -} from "@fortawesome/free-solid-svg-icons"; +import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2"; +import { Button, Checkbox, Tag } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; -import { - isConditionalSubjects, - TFormSchema, - TProjectPermissionObject -} from "../ProjectRoleModifySection.utils"; +import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils"; type Props = { title: string; @@ -28,7 +18,7 @@ type Props = { isDisabled?: boolean; }; -export const GeneralPermissionPolicies = >({ +export const GeneralPermissionOptions = >({ subject, actions, children, @@ -73,44 +63,6 @@ export const GeneralPermissionPolicies = {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
@@ -146,10 +98,10 @@ export const GeneralPermissionPolicies = - {!isDisabled && isConditionalSubjects(subject) && ( + {!isDisabled && subject === ProjectPermissionSub.Secrets && (
diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 58d8d34d99..324fa90828 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -12,7 +12,6 @@ import { PermissionDeniedBanner } from "@app/components/permissions"; import { ContentLoader, Pagination } from "@app/components/v2"; import { ProjectPermissionActions, - ProjectPermissionDynamicSecretActions, ProjectPermissionSub, useProjectPermission, useWorkspace @@ -38,7 +37,7 @@ import { ActionBar } from "./components/ActionBar"; import { CreateSecretForm } from "./components/CreateSecretForm"; import { PitDrawer } from "./components/PitDrawer"; import { SecretDropzone } from "./components/SecretDropzone"; -import { SecretListView, SecretNoAccessListView } from "./components/SecretListView"; +import { SecretListView } from "./components/SecretListView"; import { SnapshotView } from "./components/SnapshotView"; import { StoreProvider } from "./SecretMainPage.store"; import { Filter, RowType } from "./SecretMainPage.types"; @@ -80,24 +79,8 @@ export const SecretMainPage = () => { const secretPath = (router.query.secretPath as string) || "/"; const canReadSecret = permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: "*", - secretTags: ["*"] - }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); - - 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 @@ -106,12 +89,11 @@ export const SecretMainPage = () => { 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]: true, - [RowType.DynamicSecret]: true, - [RowType.Secret]: true + [RowType.Import]: canReadSecret, + [RowType.DynamicSecret]: canReadSecret, + [RowType.Secret]: canReadSecret } }; @@ -119,6 +101,19 @@ export const SecretMainPage = () => { 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 && @@ -146,9 +141,9 @@ export const SecretMainPage = () => { orderBy, search: debouncedSearchFilter, orderDirection, - includeImports: canReadSecretImports && filter.include.import, + includeImports: canReadSecret && filter.include.import, includeFolders: filter.include.folder, - includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, + includeDynamicSecrets: canReadSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags }); @@ -210,20 +205,8 @@ export const SecretMainPage = () => { 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 || - noAccessSecretCount + secrets?.length || folders?.length || imports?.length || dynamicSecrets?.length ); const handleSortToggle = () => @@ -315,6 +298,7 @@ export const SecretMainPage = () => { setFilter(defaultFilterState); setDebouncedSearchFilter(""); }; + return (
@@ -378,7 +362,7 @@ export const SecretMainPage = () => {
Value
)} - {canReadSecretImports && Boolean(imports?.length) && ( + {canReadSecret && imports?.length && ( { importedSecrets={importedSecrets} /> )} - {Boolean(folders?.length) && ( + {folders?.length && ( { onNavigateToFolder={handleResetFilter} /> )} - {canReadDynamicSecret && Boolean(dynamicSecrets?.length) && ( + {canReadSecret && dynamicSecrets?.length && ( { dynamicSecrets={dynamicSecrets} /> )} - {canReadSecret && Boolean(secrets?.length) && ( + {canReadSecret && secrets?.length && ( { isProtectedBranch={isProtectedBranch} /> )} - {canReadSecret && } - {!canReadSecret && - !canReadDynamicSecret && - !canReadSecretImports && - folders?.length === 0 && } + {!canReadSecret && folders?.length === 0 && }
{!isDetailsLoading && totalCount > 0 && ( diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 353ae3b654..333f5f0c6a 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -47,8 +47,8 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, - ProjectPermissionDynamicSecretActions, ProjectPermissionSub, + useProjectPermission, useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; @@ -125,6 +125,12 @@ export const ActionBar = ({ const { reset: resetSelectedSecret } = useSelectedSecretActions(); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); + const { permission } = useProjectPermission(); + + const shouldCheckFolderPermission = permission.rules.some((rule) => + (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) + ); + const handleFolderCreate = async (folderName: string) => { try { await createFolder({ @@ -432,12 +438,7 @@ export const ActionBar = ({
{(isAllowed) => ( - )} - + diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx index 8953ab2bc0..abef7b6c30 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 { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteDynamicSecret } from "@app/hooks/api"; import { @@ -132,27 +132,17 @@ export const DynamicSecretListView = ({ )}
- { + evt.stopPropagation(); + handlePopUpOpen("createDynamicSecretLease", secret); + }} > - {(isAllowed) => ( - - )} - - + Generate + {secret.status === DynamicSecretStatus.FailedDeletion && ( @@ -419,12 +388,7 @@ export const SecretDetailSidebar = ({ render={({ field: { value, onChange, onBlur } }) => ( {(isAllowed) => ( {(isAllowed) => (
- + {(isAllowed) => ( { )}
- + {(isAllowed) => (