diff --git a/backend/package-lock.json b/backend/package-lock.json index 7d1f95ad81..9937e8549f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -5852,12 +5852,12 @@ } }, "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==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-2.5.0.tgz", + "integrity": "sha512-I7zI6MWP1wz9qvTY8U3wOWeRXY2NiuTDqf91v/LQl9oiffUHl+Z1YelRvNcvHbaUo/GK7E1mJr+Sw4dHuSGxpg==", "license": "MIT", "dependencies": { - "@sentry/node": "^6.0.0", + "@sentry/node": "^7.119.2", "pino-pretty": "^6.0.0", "pump": "^3.0.0", "readable-stream": "^3.6.0", @@ -6147,118 +6147,85 @@ "win32" ] }, - "node_modules/@sentry/core": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.19.7.tgz", - "integrity": "sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw==", - "dependencies": { - "@sentry/hub": "6.19.7", - "@sentry/minimal": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", - "tslib": "^1.9.3" + "node_modules/@sentry-internal/tracing": { + "version": "7.119.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.119.2.tgz", + "integrity": "sha512-V2W+STWrafyGJhQv3ulMFXYDwWHiU6wHQAQBShsHVACiFaDrJ2kPRet38FKv4dMLlLlP2xN+ss2e5zv3tYlTiQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.119.2", + "@sentry/types": "7.119.2", + "@sentry/utils": "7.119.2" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/@sentry/core/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/hub": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.19.7.tgz", - "integrity": "sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA==", + "node_modules/@sentry/core": { + "version": "7.119.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.119.2.tgz", + "integrity": "sha512-hQr3d2yWq/2lMvoyBPOwXw1IHqTrCjOsU1vYKhAa6w9vGbJZFGhKGGE2KEi/92c3gqGn+gW/PC7cV6waCTDuVA==", + "license": "MIT", "dependencies": { - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", - "tslib": "^1.9.3" + "@sentry/types": "7.119.2", + "@sentry/utils": "7.119.2" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/@sentry/hub/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/minimal": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.19.7.tgz", - "integrity": "sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==", + "node_modules/@sentry/integrations": { + "version": "7.119.2", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.119.2.tgz", + "integrity": "sha512-dCuXKvbUE3gXVVa696SYMjlhSP6CxpMH/gl4Jk26naEB8Xjsn98z/hqEoXLg6Nab73rjR9c/9AdKqBbwVMHyrQ==", + "license": "MIT", "dependencies": { - "@sentry/hub": "6.19.7", - "@sentry/types": "6.19.7", - "tslib": "^1.9.3" + "@sentry/core": "7.119.2", + "@sentry/types": "7.119.2", + "@sentry/utils": "7.119.2", + "localforage": "^1.8.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/@sentry/minimal/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/@sentry/node": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-6.19.7.tgz", - "integrity": "sha512-gtmRC4dAXKODMpHXKfrkfvyBL3cI8y64vEi3fDD046uqYcrWdgoQsffuBbxMAizc6Ez1ia+f0Flue6p15Qaltg==", - "dependencies": { - "@sentry/core": "6.19.7", - "@sentry/hub": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^1.9.3" + "version": "7.119.2", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.119.2.tgz", + "integrity": "sha512-TPNnqxh+Myooe4jTyRiXrzrM2SH08R4+nrmBls4T7lKp2E5R/3mDSe/YTn5rRcUt1k1hPx1NgO/taG0DoS5cXA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.119.2", + "@sentry/core": "7.119.2", + "@sentry/integrations": "7.119.2", + "@sentry/types": "7.119.2", + "@sentry/utils": "7.119.2" }, "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/node/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/@sentry/node/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/@sentry/types": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", - "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "version": "7.119.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.119.2.tgz", + "integrity": "sha512-ydq1tWsdG7QW+yFaTp0gFaowMLNVikIqM70wxWNK+u98QzKnVY/3XTixxNLsUtnAB4Y+isAzFhrc6Vb5GFdFeg==", + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.19.7.tgz", - "integrity": "sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==", + "version": "7.119.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.119.2.tgz", + "integrity": "sha512-TLdUCvcNgzKP0r9YD7tgCL1PEUp42TObISridsPJ5rhpVGQJvpr+Six0zIkfDUxerLYWZoK8QMm9KgFlPLNQzA==", + "license": "MIT", "dependencies": { - "@sentry/types": "6.19.7", - "tslib": "^1.9.3" + "@sentry/types": "7.119.2" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/@sentry/utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/@serdnam/pino-cloudwatch-transport": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@serdnam/pino-cloudwatch-transport/-/pino-cloudwatch-transport-1.0.4.tgz", @@ -9728,9 +9695,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==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -10863,9 +10830,9 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -10873,7 +10840,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -10905,12 +10872,13 @@ } }, "node_modules/express-session": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", - "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", "peer": true, "dependencies": { - "cookie": "0.6.0", + "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", @@ -10944,6 +10912,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "peer": true }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "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", @@ -12424,6 +12401,12 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -13420,13 +13403,22 @@ "libsodium": "^0.7.13" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "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==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", "license": "BSD-3-Clause", "dependencies": { - "cookie": "^0.6.0", + "cookie": "^0.7.0", "process-warning": "^3.0.0", "set-cookie-parser": "^2.4.1" } @@ -13505,6 +13497,15 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -13632,11 +13633,6 @@ "get-func-name": "^2.0.1" } }, - "node_modules/lru_map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -15261,9 +15257,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -18865,9 +18861,9 @@ } }, "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.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", + "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 354632d156..c6228e35a6 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -669,6 +669,12 @@ export const RAW_SECRETS = { type: "The type of the secret to delete.", projectSlug: "The slug of the project to delete the secret in.", workspaceId: "The ID of the project where the secret is located." + }, + GET_REFERENCE_TREE: { + secretName: "The name of the secret to get the reference tree for.", + workspaceId: "The ID of the project where the secret is located.", + environment: "The slug of the environment where the the secret is located.", + secretPath: "The folder path where the secret is located." } } as const; diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index e0341cc78b..61981bef5a 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -23,6 +23,18 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { secretRawSchema } from "../sanitizedSchemas"; +const SecretReferenceNode = z.object({ + key: z.string(), + value: z.string().optional(), + environment: z.string(), + secretPath: z.string() +}); +type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; + +const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ + children: z.lazy(() => SecretReferenceNodeTree.array()) +}); + export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -2102,6 +2114,58 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/raw/:secretName/secret-reference-tree", + config: { + rateLimit: secretsLimit + }, + schema: { + description: "Get secret reference tree", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) + }), + querystring: z.object({ + workspaceId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId), + environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath) + }), + response: { + 200: z.object({ + tree: SecretReferenceNodeTree, + value: z.string().optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretName } = req.params; + const { secretPath, environment, workspaceId } = req.query; + const { tree, value } = await server.services.secret.getSecretReferenceTree({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: workspaceId, + secretName, + secretPath, + environment + }); + + return { tree, value }; + } + }); + server.route({ method: "POST", url: "/backfill-secret-references", 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 845b8d5698..28fd1c5bbc 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 @@ -11,6 +11,8 @@ import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g; +// akhilmhdh: JS regex with global save state in .test +const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([^}]+)}/; export const shouldUseSecretV2Bridge = (version: number) => version === 3; @@ -376,6 +378,13 @@ const formatMultiValueEnv = (val?: string) => { return `"${val.replace(/\n/g, "\\n")}"`; }; +type TSecretReferenceTraceNode = { + key: string; + value?: string; + environment: string; + secretPath: string; + children: TSecretReferenceTraceNode[]; +}; type TInterpolateSecretArg = { projectId: string; decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined; @@ -417,14 +426,21 @@ export const expandSecretReferencesFactory = ({ return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; }; - const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => { - if (!dto.value) return ""; + const recursivelyExpandSecret = async (dto: { + value?: string; + secretPath: string; + environment: string; + shouldStackTrace?: boolean; + }) => { + const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode; - const stack = [{ ...dto, depth: 0 }]; + if (!dto.value) return { expandedValue: "", stackTrace }; + const stack = [{ ...dto, depth: 0, trace: stackTrace }]; let expandedValue = dto.value; while (stack.length) { - const { value, secretPath, environment, depth } = stack.pop()!; + const { value, secretPath, environment, depth, trace } = stack.pop()!; + // eslint-disable-next-line no-continue if (depth > MAX_SECRET_REFERENCE_DEPTH) continue; const refs = value?.match(INTERPOLATION_SYNTAX_REG); @@ -437,6 +453,11 @@ export const expandSecretReferencesFactory = ({ // eslint-disable-next-line no-continue if (!entities.length) continue; + let referencedSecretPath = ""; + let referencedSecretKey = ""; + let referencedSecretEnvironmentSlug = ""; + let referencedSecretValue = ""; + if (entities.length === 1) { const [secretKey] = entities; @@ -449,17 +470,11 @@ export const expandSecretReferencesFactory = ({ const cacheKey = getCacheUniqueKey(environment, secretPath); secretCache[cacheKey][secretKey] = referredValue; - if (INTERPOLATION_SYNTAX_REG.test(referredValue.value)) { - stack.push({ - value: referredValue.value, - secretPath, - environment, - depth: depth + 1 - }); - } - if (referredValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referredValue.value); - } + + referencedSecretValue = referredValue.value; + referencedSecretKey = secretKey; + referencedSecretPath = secretPath; + referencedSecretEnvironmentSlug = environment; } else { const secretReferenceEnvironment = entities[0]; const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); @@ -474,24 +489,42 @@ export const expandSecretReferencesFactory = ({ const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); secretCache[cacheKey][secretReferenceKey] = referedValue; - if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) { - stack.push({ - value: referedValue.value, - secretPath: secretReferencePath, - environment: secretReferenceEnvironment, - depth: depth + 1 - }); - } - if (referedValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value); + referencedSecretValue = referedValue.value; + referencedSecretKey = secretReferenceKey; + referencedSecretPath = secretReferencePath; + referencedSecretEnvironmentSlug = secretReferenceEnvironment; + } + + const node = { + value: referencedSecretValue, + secretPath: referencedSecretPath, + environment: referencedSecretEnvironmentSlug, + depth: depth + 1, + trace + }; + + const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue); + if (dto.shouldStackTrace) { + const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null }; + trace?.children.push(stackTraceNode); + // if stack trace this would be child node + if (shouldExpandMore) { + stack.push({ ...node, trace: stackTraceNode }); } + } else if (shouldExpandMore) { + // if no stack trace is needed we just keep going with root node + stack.push(node); + } + + if (referencedSecretValue) { + expandedValue = expandedValue.replaceAll(interpolationSyntax, referencedSecretValue); } } } } - return expandedValue; + return { expandedValue, stackTrace }; }; const expandSecret = async (inputSecret: { @@ -505,10 +538,21 @@ export const expandSecretReferencesFactory = ({ const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG)); if (!shouldExpand) return inputSecret.value; - const expandedSecretValue = await recursivelyExpandSecret(inputSecret); - return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue; + const { expandedValue } = await recursivelyExpandSecret(inputSecret); + + return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedValue) : expandedValue; }; - return expandSecret; + + const getExpandedSecretStackTrace = async (inputSecret: { + value?: string; + secretPath: string; + environment: string; + }) => { + const { stackTrace, expandedValue } = await recursivelyExpandSecret({ ...inputSecret, shouldStackTrace: true }); + return { stackTrace, expandedValue }; + }; + + return { expandSecretReferences: expandSecret, getExpandedSecretStackTrace }; }; export const reshapeBridgeSecret = ( 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 756e1f8105..78d771cd51 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 @@ -41,6 +41,7 @@ import { TDeleteManySecretDTO, TDeleteSecretDTO, TGetASecretDTO, + TGetSecretReferencesTreeDTO, TGetSecretsDTO, TGetSecretVersionsDTO, TMoveSecretsDTO, @@ -815,7 +816,7 @@ export const secretV2BridgeServiceFactory = ({ }) ); - const expandSecretReferences = expandSecretReferencesFactory({ + const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, @@ -965,7 +966,7 @@ export const secretV2BridgeServiceFactory = ({ }) ); - const expandSecretReferences = expandSecretReferencesFactory({ + const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, @@ -1032,6 +1033,7 @@ export const secretV2BridgeServiceFactory = ({ value: secretValue, skipMultilineEncoding: secret.skipMultilineEncoding }); + secretValue = expandedSecretValue || ""; } @@ -1928,6 +1930,88 @@ export const secretV2BridgeServiceFactory = ({ }; }; + const getSecretReferenceTree = async ({ + environment, + secretPath, + projectId, + actor, + actorId, + actorOrgId, + secretName, + actorAuthMethod + }: TGetSecretReferencesTreeDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) + throw new NotFoundError({ + message: "Folder not found for the given environment slug & secret path", + name: "Create secret" + }); + const folderId = folder.id; + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const secret = await secretDAL.findOne({ + folderId, + key: secretName, + type: SecretType.Shared + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ); + + const secretValue = secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : ""; + + const { getExpandedSecretStackTrace } = expandSecretReferencesFactory({ + projectId, + folderDAL, + secretDAL, + decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretName, + secretTags: expandSecretTags + }) + ) + }); + + const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ + environment, + secretPath, + value: secretValue + }); + + return { tree: stackTrace, value: expandedValue }; + }; + return { createSecret, deleteSecret, @@ -1942,6 +2026,7 @@ export const secretV2BridgeServiceFactory = ({ moveSecrets, getSecretsCount, getSecretsCountMultiEnv, - getSecretsMultiEnv + getSecretsMultiEnv, + getSecretReferenceTree }; }; 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..8a3e08e35a 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 @@ -278,3 +278,10 @@ export type TAttachSecretTagsDTO = { secretPath: string; type: SecretType; } & Omit; + +export type TGetSecretReferencesTreeDTO = { + projectId: string; + secretName: string; + environment: string; + secretPath: string; +} & Omit; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index f98a78252b..4b41144b47 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -299,7 +299,7 @@ export const secretQueueFactory = ({ ); return content; } - const expandSecretReferences = expandSecretReferencesFactory({ + const { expandSecretReferences } = expandSecretReferencesFactory({ decryptSecretValue: dto.decryptor, secretDAL: secretV2BridgeDAL, folderDAL, diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index eaa5f5c4ee..13eca368f3 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -38,6 +38,7 @@ import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service"; +import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretDALFactory } from "./secret-dal"; import { decryptSecretRaw, @@ -1099,6 +1100,18 @@ export const secretServiceFactory = ({ return secrets; }; + const getSecretReferenceTree = async (dto: TGetSecretReferencesTreeDTO) => { + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(dto.projectId); + + if (!shouldUseSecretV2Bridge) + throw new BadRequestError({ + message: "Project version does not support secret reference tree", + name: "SecretReferenceTreeNotSupported" + }); + + return secretV2BridgeService.getSecretReferenceTree(dto); + }; + const getSecretsRaw = async ({ projectId, path, @@ -2857,6 +2870,7 @@ export const secretServiceFactory = ({ startSecretV2Migration, getSecretsCount, getSecretsCountMultiEnv, - getSecretsRawMultiEnv + getSecretsRawMultiEnv, + getSecretReferenceTree }; }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0111e54a37..8505294bbd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", @@ -4931,6 +4932,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", diff --git a/frontend/package.json b/frontend/package.json index d4a820f8b1..b219093a6a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts index 737ebf41a3..74d6c4439a 100644 --- a/frontend/src/hooks/api/secrets/index.ts +++ b/frontend/src/hooks/api/secrets/index.ts @@ -8,4 +8,8 @@ export { useUpdateSecretBatch, useUpdateSecretV3 } from "./mutations"; -export { useGetProjectSecrets, useGetProjectSecretsAllEnv, useGetSecretVersion } from "./queries"; +export { + useGetProjectSecrets, + useGetProjectSecretsAllEnv, + useGetSecretReferenceTree, + useGetSecretVersion} from "./queries"; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 1568930c6d..dff4d18a06 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -17,14 +17,17 @@ import { SecretVersions, TGetProjectSecretsAllEnvDTO, TGetProjectSecretsDTO, - TGetProjectSecretsKey + TGetProjectSecretsKey, + TGetSecretReferenceTreeDTO, + TSecretReferenceTraceNode } from "./types"; export const secretKeys = { // this is also used in secretSnapshot part getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) => [{ workspaceId, environment, secretPath }, "secrets"] as const, - getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const + getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, + getSecretReferenceTree: (dto: TGetSecretReferenceTreeDTO) => ["secret-reference-tree", dto] }; export const fetchProjectSecrets = async ({ @@ -227,3 +230,33 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => return data.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); }, []) }); + +const fetchSecretReferenceTree = async ({ + secretPath, + projectId, + secretKey, + environmentSlug +}: TGetSecretReferenceTreeDTO) => { + const { data } = await apiRequest.get<{ tree: TSecretReferenceTraceNode; value: string }>( + `/api/v3/secrets/raw/${secretKey}/secret-reference-tree`, + { + params: { + secretPath, + workspaceId: projectId, + environment: environmentSlug + } + } + ); + return data; +}; + +export const useGetSecretReferenceTree = (dto: TGetSecretReferenceTreeDTO) => + useQuery({ + enabled: + Boolean(dto.environmentSlug) && + Boolean(dto.secretPath) && + Boolean(dto.projectId) && + Boolean(dto.secretKey), + queryKey: secretKeys.getSecretReferenceTree(dto), + queryFn: () => fetchSecretReferenceTree(dto) + }); diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index ff87a363a3..9566851788 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -210,3 +210,18 @@ export type TMoveSecretsDTO = { secretIds: string[]; shouldOverwrite: boolean; }; + +export type TGetSecretReferenceTreeDTO = { + secretKey: string; + secretPath: string; + environmentSlug: string; + projectId: string; +}; + +export type TSecretReferenceTraceNode = { + key: string; + value?: string; + environment: string; + secretPath: string; + children: TSecretReferenceTraceNode[]; +}; diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index d3aa5c7f02..0ccfbc4667 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -137,6 +137,27 @@ html { ); } +.tree-line::before { + content: ""; + position: absolute; + left: -16px; + top: 1px; + bottom: 0; + width: 1px; + height: 50%; + background-color: #cbd5e0; +} + +.tree-line::after { + content: ""; + position: absolute; + left: -16px; + top: 50%; + width: 12px; + height: 1px; + background-color: #cbd5e0; +} + .show-tags { transform: translateY(10px); transition: all 0.2s; diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetailSidebar.tsx similarity index 99% rename from frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx rename to frontend/src/views/SecretMainPage/components/SecretListView/SecretDetailSidebar.tsx index d279e2398b..6aa7fd4e1e 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetailSidebar.tsx @@ -393,7 +393,7 @@ export const SecretDetailSidebar = ({ ) : (
-
+
- - - + + + + e.preventDefault()} // prevents secret input from displaying value on open + > + + + + )} + + {(isAllowed) => ( - - - + + + + + + + )} + + Add tags to this secret + {tags.map((tag) => { + const { id: tagId, slug, color } = tag; - {!isOverriden && ( + const isTagSelected = selectedTagsGroupById?.[tagId]; + return ( + handleTagSelect(tag)} + key={`${secret.id}-${tagId}`} + icon={ + isTagSelected && ( + + ) + } + iconPos="right" + > +
+
+ {slug} +
+ + ); + })} + + + + + + + {(isAllowed) => ( setCreateReminderFormOpen.on()} > - 0 - ? `Every ${secretReminderRepeatDays} day${ - Number(secretReminderRepeatDays) > 1 ? "s" : "" - } - ` - : "Reminder" - } - > - - + )} - - - - {(isAllowed) => ( - - - - - - - - )} - + + + + {(isAllowed) => ( + + + + + + + + )} + + + + + + + + +