diff --git a/backend-pg/e2e-test/mocks/queue.ts b/backend-pg/e2e-test/mocks/queue.ts new file mode 100644 index 000000000..22dd8dc80 --- /dev/null +++ b/backend-pg/e2e-test/mocks/queue.ts @@ -0,0 +1,23 @@ +import { TQueueServiceFactory } from "@app/queue"; + +export const mockQueue = (): TQueueServiceFactory => { + const queues: Record = {}; + const workers: Record = {}; + const job: Record = {}; + const events: Record = {}; + + return { + queue: async (name, jobData) => { + job[name] = jobData; + }, + shutdown: async () => undefined, + stopRepeatableJob: async () => true, + start: (name, jobFn) => { + queues[name] = jobFn; + workers[name] = jobFn; + }, + listen: async (name, event) => { + events[name] = event; + } + }; +}; diff --git a/backend-pg/e2e-test/vitest-environment-knex.ts b/backend-pg/e2e-test/vitest-environment-knex.ts index a52f21175..1f98d787b 100644 --- a/backend-pg/e2e-test/vitest-environment-knex.ts +++ b/backend-pg/e2e-test/vitest-environment-knex.ts @@ -8,6 +8,7 @@ import { initLogger } from "@app/lib/logger"; import "ts-node/register"; import { main } from "@app/server/app"; +import { mockQueue } from "./mocks/queue"; dotenv.config({ path: path.join(__dirname, "../.env.test") }); export default { @@ -31,9 +32,10 @@ export default { await db.migrate.latest(); await db.seed.run(); const smtp = mockSmtpServer(); + const queue = mockQueue(); const logger = await initLogger(); initEnvConfig(logger); - const server = await main({ db, smtp, logger }); + const server = await main({ db, smtp, logger, queue }); // @ts-expect-error type globalThis.testServer = server; } catch (error) { diff --git a/backend-pg/folder.ts b/backend-pg/folder.ts index dc7d6be5b..041fe8f74 100644 --- a/backend-pg/folder.ts +++ b/backend-pg/folder.ts @@ -3,55 +3,87 @@ import dotenv from "dotenv"; import { initDbConnection } from "./src/db"; import { TableName } from "./src/db/schemas"; import { selectAllTableCols } from "./src/lib/knex"; +import picomatch from "picomatch"; dotenv.config(); -const db = initDbConnection(process.env.DB_CONNECTION_URI); +// const db = initDbConnection(process.env.DB_CONNECTION_URI); +// const main = async () => { +// const folders = db +// .withRecursive("parent", (qb) => { +// qb.select({ +// depth: 1, +// path: db.raw("'/'") +// }) +// .select(selectAllTableCols(db, TableName.SecretFolder)) +// .from(TableName.SecretFolder) +// .join( +// TableName.Environment, +// `${TableName.SecretFolder}.envId`, +// `${TableName.Environment}.id` +// ) +// .where({ +// projectId: "01c10de1-8743-490f-9c8a-7a19c4dc72a9", +// parentId: null +// }) +// .where(`${TableName.Environment}.slug`, "dev") +// .union((qb) => +// qb +// .select({ +// depth: db.raw("parent.depth + 1"), +// path: db.raw( +// "CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)" +// ) +// }) +// .select(selectAllTableCols(db, TableName.SecretFolder)) +// .whereRaw( +// `depth = array_position(ARRAY[${[1, 2] +// .map((_) => "?") +// .join(",")}]::varchar[], secret_folders.name,depth)`, +// [...["ui", "design"]] +// ) +// .from(TableName.SecretFolder) +// .join("parent", "parent.id", `${TableName.SecretFolder}.parentId`) +// ); +// }) +// .select("*") +// .from("parent") +// .orderBy("depth", "desc") +// .first(); +// console.log(folders.toSQL()); +// console.log(JSON.stringify(await folders, null, 4)); +// process.exit(0); +// }; +// const main = async () => { - const folders = db - .withRecursive("parent", (qb) => { - qb.select({ - depth: 1, - path: db.raw("'/'") - }) - .select(selectAllTableCols(db, TableName.SecretFolder)) - .from(TableName.SecretFolder) - .join( - TableName.Environment, - `${TableName.SecretFolder}.envId`, - `${TableName.Environment}.id` - ) - .where({ - projectId: "01c10de1-8743-490f-9c8a-7a19c4dc72a9", - parentId: null - }) - .where(`${TableName.Environment}.slug`, "dev") - .union((qb) => - qb - .select({ - depth: db.raw("parent.depth + 1"), - path: db.raw( - "CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)" - ) - }) - .select(selectAllTableCols(db, TableName.SecretFolder)) - .whereRaw( - `depth = array_position(ARRAY[${[1, 2] - .map((_) => "?") - .join(",")}]::varchar[], secret_folders.name,depth)`, - [...["ui", "design"]] - ) - .from(TableName.SecretFolder) - .join("parent", "parent.id", `${TableName.SecretFolder}.parentId`) - ); - }) - .select("*") - .from("parent") - .orderBy("depth", "desc") - .first(); - console.log(folders.toSQL()); - console.log(JSON.stringify(await folders, null, 4)); - process.exit(0); + const isMatch = picomatch("/ui/**/seomthing", { strictSlashes: false }); + console.log( + picomatch.isMatch( + "/ui/(base|new)/seomthing", + ["/ui/base/seomthing", "/ui/(base|new)/seomthing"], + { + strictSlashes: false + } + ) + ); + // console.log(isMatch("/ui/(base|new)/seomthing")); + // const folders = db(TableName.SecretVersion) + // .whereIn(`${TableName.SecretVersion}.secretId`, ["8fe1c5e2-af6a-40ed-874b-6738816236bc"]) + // .join( + // db(TableName.SecretVersion) + // .groupBy("secretId") + // .max("version") + // .select("secretId") + // .as("latestVersion"), + // (bd) => { + // bd.on(`${TableName.SecretVersion}.secretId`, "latestVersion.secretId").andOn( + // `${TableName.SecretVersion}.version`, + // "latestVersion.max" + // ); + // } + // ); + // console.log(folders.toSQL()); + // console.log(JSON.stringify(await folders, null, 4)); + // process.exit(0); }; - main(); diff --git a/backend-pg/package-lock.json b/backend-pg/package-lock.json index 804392f7b..8068ad6b8 100644 --- a/backend-pg/package-lock.json +++ b/backend-pg/package-lock.json @@ -20,18 +20,23 @@ "@fastify/swagger-ui": "^1.10.1", "@octokit/rest": "^20.0.2", "@ucast/mongo2js": "^1.3.4", + "ajv": "^8.12.0", "argon2": "^0.31.2", "axios": "^1.6.2", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", + "bullmq": "^5.1.1", "dotenv": "^16.3.1", "eslint-config-airbnb-typescript": "^17.1.0", "fastify": "^4.24.3", "fastify-plugin": "^4.5.1", "handlebars": "^4.7.8", + "ioredis": "^5.3.2", + "jmespath": "^0.16.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", "knex": "^3.0.1", + "mysql2": "^3.6.5", "nanoid": "^5.0.4", "nodemailer": "^6.9.7", "ora": "^7.0.1", @@ -48,6 +53,7 @@ }, "devDependencies": { "@types/bcrypt": "^5.0.2", + "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "^9.0.5", "@types/jsrp": "^0.2.6", "@types/node": "^20.9.5", @@ -753,6 +759,11 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -913,6 +924,78 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", + "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", + "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", + "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", + "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", + "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", + "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1602,6 +1685,12 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/jmespath": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz", + "integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2803,6 +2892,22 @@ "node": ">=4" } }, + "node_modules/bullmq": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.1.1.tgz", + "integrity": "sha512-j3zbNEQWsyHjpqGWiem2XBfmxAjYcArbwsmGlkM1E9MAVcrqB5hQUsXmyy9gEBAdL+PVotMICr7xTquR4Y2sKQ==", + "dependencies": { + "cron-parser": "^4.6.0", + "glob": "^8.0.3", + "ioredis": "^5.3.2", + "lodash": "^4.17.21", + "msgpackr": "^1.6.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -2973,6 +3078,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3082,6 +3195,17 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3217,6 +3341,14 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4463,6 +4595,14 @@ "node": ">=8" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -4876,6 +5016,17 @@ "node": ">=14.18.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4967,6 +5118,50 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/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/ioredis/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/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5174,6 +5369,11 @@ "node": ">=8" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -5342,6 +5542,14 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -5656,11 +5864,21 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -5717,6 +5935,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -5737,6 +5960,14 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -5948,6 +6179,61 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/msgpackr": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", + "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", + "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.0.7" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + } + }, + "node_modules/mysql2": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.5.tgz", + "integrity": "sha512-pS/KqIb0xlXmtmqEuTvBXTmLoQ5LmAz5NW/r8UyQ1ldvnprNEj3P9GbmuQQ2J0A4LO+ynotGi6TbscPa8OUb+w==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -5959,6 +6245,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz", @@ -5986,6 +6291,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -6010,6 +6320,17 @@ } } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "optional": true, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/nodemailer": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz", @@ -7073,6 +7394,25 @@ "node": ">= 10.13.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -7436,6 +7776,11 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -7455,6 +7800,11 @@ "node": ">=10" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -7605,12 +7955,25 @@ "node": ">= 10.x" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -8178,8 +8541,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsup": { "version": "8.0.1", @@ -8971,6 +9333,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/backend-pg/package.json b/backend-pg/package.json index 811ec61d3..0e2a09935 100644 --- a/backend-pg/package.json +++ b/backend-pg/package.json @@ -32,6 +32,7 @@ "license": "ISC", "devDependencies": { "@types/bcrypt": "^5.0.2", + "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "^9.0.5", "@types/jsrp": "^0.2.6", "@types/node": "^20.9.5", @@ -73,18 +74,23 @@ "@fastify/swagger-ui": "^1.10.1", "@octokit/rest": "^20.0.2", "@ucast/mongo2js": "^1.3.4", + "ajv": "^8.12.0", "argon2": "^0.31.2", "axios": "^1.6.2", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", + "bullmq": "^5.1.1", "dotenv": "^16.3.1", "eslint-config-airbnb-typescript": "^17.1.0", "fastify": "^4.24.3", "fastify-plugin": "^4.5.1", "handlebars": "^4.7.8", + "ioredis": "^5.3.2", + "jmespath": "^0.16.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", "knex": "^3.0.1", + "mysql2": "^3.6.5", "nanoid": "^5.0.4", "nodemailer": "^6.9.7", "ora": "^7.0.1", diff --git a/backend-pg/scripts/create-backend-file.ts b/backend-pg/scripts/create-backend-file.ts index 907ebe2c4..d2b391051 100644 --- a/backend-pg/scripts/create-backend-file.ts +++ b/backend-pg/scripts/create-backend-file.ts @@ -113,7 +113,7 @@ export const register${pascalCase}Router = async (server: FastifyZodProvider) => response: { 200: z.object({}) } - } + }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => {} }); diff --git a/backend-pg/src/@types/fastify.d.ts b/backend-pg/src/@types/fastify.d.ts index f1dabfca1..b90ade2ba 100644 --- a/backend-pg/src/@types/fastify.d.ts +++ b/backend-pg/src/@types/fastify.d.ts @@ -2,6 +2,9 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; +import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service"; +import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; @@ -23,7 +26,6 @@ import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { TSecretServiceFactory } from "@app/services/secret/secret-service"; -import { TSecretApprovalPolicyServiceFactory } from "@app/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service"; import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service"; @@ -92,6 +94,8 @@ declare module "fastify" { identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; + secretApprovalRequest: TSecretApprovalRequestServiceFactory; + secretRotation: TSecretRotationServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data diff --git a/backend-pg/src/@types/knex.d.ts b/backend-pg/src/@types/knex.d.ts index 8d6550bbb..e1174b0f5 100644 --- a/backend-pg/src/@types/knex.d.ts +++ b/backend-pg/src/@types/knex.d.ts @@ -94,6 +94,12 @@ import { TSecretImports, TSecretImportsInsert, TSecretImportsUpdate, + TSecretRotationOutputs, + TSecretRotationOutputsInsert, + TSecretRotationOutputsUpdate, + TSecretRotations, + TSecretRotationsInsert, + TSecretRotationsUpdate, TSecrets, TSecretsInsert, TSecretsUpdate, @@ -304,6 +310,16 @@ declare module "knex/types/tables" { TSaRequestSecretTagsInsert, TSaRequestSecretTagsUpdate >; + [TableName.SecretRotation]: Knex.CompositeTableType< + TSecretRotations, + TSecretRotationsInsert, + TSecretRotationsUpdate + >; + [TableName.SecretRotationOutput]: Knex.CompositeTableType< + TSecretRotationOutputs, + TSecretRotationOutputsInsert, + TSecretRotationOutputsUpdate + >; // Junction tables [TableName.JnSecretTag]: Knex.CompositeTableType< TSecretTagJunction, diff --git a/backend-pg/src/cache/redis.ts b/backend-pg/src/cache/redis.ts new file mode 100644 index 000000000..4e856fac1 --- /dev/null +++ b/backend-pg/src/cache/redis.ts @@ -0,0 +1,6 @@ +import Redis from "ioredis"; + +export const initRedisConnection = (redisUrl: string) => { + const redis = new Redis(redisUrl); + return redis; +}; diff --git a/backend-pg/src/db/migrations/20240101054849_secret-approval-policy.ts b/backend-pg/src/db/migrations/20240101054849_secret-approval-policy.ts index ac2e01290..10d9a1480 100644 --- a/backend-pg/src/db/migrations/20240101054849_secret-approval-policy.ts +++ b/backend-pg/src/db/migrations/20240101054849_secret-approval-policy.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; + import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; diff --git a/backend-pg/src/db/migrations/20240101104907_secret-approval-request.ts b/backend-pg/src/db/migrations/20240101104907_secret-approval-request.ts index 5f6981ecf..d2eae9416 100644 --- a/backend-pg/src/db/migrations/20240101104907_secret-approval-request.ts +++ b/backend-pg/src/db/migrations/20240101104907_secret-approval-request.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; + import { SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; diff --git a/backend-pg/src/db/migrations/20240102152111_secret-rotation.ts b/backend-pg/src/db/migrations/20240102152111_secret-rotation.ts new file mode 100644 index 000000000..f7009488a --- /dev/null +++ b/backend-pg/src/db/migrations/20240102152111_secret-rotation.ts @@ -0,0 +1,47 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.SecretRotation))) { + await knex.schema.createTable(TableName.SecretRotation, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("provider").notNullable(); + t.string("secretPath").notNullable(); + t.integer("interval").notNullable(); + t.datetime("lastRotatedAt"); + t.string("status"); + t.text("statusMessage"); + t.text("encryptedData"); + t.text("encryptedDataIV"); + t.text("encryptedDataTag"); + t.string("algorithm"); + t.string("keyEncoding"); + t.uuid("envId").notNullable(); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + await createOnUpdateTrigger(knex, TableName.SecretRotation); + + if (!(await knex.schema.hasTable(TableName.SecretRotationOutput))) { + await knex.schema.createTable(TableName.SecretRotationOutput, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("key").notNullable(); + t.uuid("secretId").notNullable(); + t.foreign("secretId").references("id").inTable(TableName.Secret).onDelete("CASCADE"); + t.uuid("rotationId").notNullable(); + t.foreign("rotationId") + .references("id") + .inTable(TableName.SecretRotation) + .onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.SecretRotationOutput); + await knex.schema.dropTableIfExists(TableName.SecretRotation); + await dropOnUpdateTrigger(knex, TableName.SecretRotation); +} diff --git a/backend-pg/src/db/schemas/index.ts b/backend-pg/src/db/schemas/index.ts index 78b1cd7bd..2f04438dc 100644 --- a/backend-pg/src/db/schemas/index.ts +++ b/backend-pg/src/db/schemas/index.ts @@ -30,6 +30,8 @@ export * from "./secret-approval-requests"; export * from "./secret-blind-indexes"; export * from "./secret-folders"; export * from "./secret-imports"; +export * from "./secret-rotation-outputs"; +export * from "./secret-rotations"; export * from "./secret-tag-junction"; export * from "./secret-tags"; export * from "./secret-versions"; diff --git a/backend-pg/src/db/schemas/models.ts b/backend-pg/src/db/schemas/models.ts index 133fac1ca..9c1d0cd16 100644 --- a/backend-pg/src/db/schemas/models.ts +++ b/backend-pg/src/db/schemas/models.ts @@ -41,6 +41,8 @@ export enum TableName { SarReviewer = "sar_reviewers", SarSecret = "sa_request_secrets", SarSecretTag = "sa_request_secret_tags", + SecretRotation = "secret_rotations", + SecretRotationOutput = "secret_rotation_outputs", // junction tables JnSecretTag = "secret_tag_junction", JnSecretVersionTag = "secret_version_tag_junction" diff --git a/backend-pg/src/db/schemas/sa-request-secrets.ts b/backend-pg/src/db/schemas/sa-request-secrets.ts index 80f2376fb..6cbaeca0a 100644 --- a/backend-pg/src/db/schemas/sa-request-secrets.ts +++ b/backend-pg/src/db/schemas/sa-request-secrets.ts @@ -23,15 +23,15 @@ export const SaRequestSecretsSchema = z.object({ secretReminderNotice: z.string().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(), - algorithm: z.string().default('aes-256-gcm'), - keyEncoding: z.string().default('utf8'), + algorithm: z.string().default("aes-256-gcm"), + keyEncoding: z.string().default("utf8"), metadata: z.unknown().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), requestId: z.string().uuid(), op: z.string(), secretId: z.string().uuid().nullable().optional(), - secretVersion: z.string().uuid().nullable().optional(), + secretVersion: z.string().uuid().nullable().optional() }); export type TSaRequestSecrets = z.infer; diff --git a/backend-pg/src/db/schemas/secret-rotation-outputs.ts b/backend-pg/src/db/schemas/secret-rotation-outputs.ts new file mode 100644 index 000000000..b98338c75 --- /dev/null +++ b/backend-pg/src/db/schemas/secret-rotation-outputs.ts @@ -0,0 +1,19 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SecretRotationOutputsSchema = z.object({ + id: z.string().uuid(), + key: z.string(), + secretId: z.string().uuid(), + rotationId: z.string().uuid(), +}); + +export type TSecretRotationOutputs = z.infer; +export type TSecretRotationOutputsInsert = Omit; +export type TSecretRotationOutputsUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/secret-rotations.ts b/backend-pg/src/db/schemas/secret-rotations.ts new file mode 100644 index 000000000..6e2bf6547 --- /dev/null +++ b/backend-pg/src/db/schemas/secret-rotations.ts @@ -0,0 +1,30 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SecretRotationsSchema = z.object({ + id: z.string().uuid(), + provider: z.string(), + secretPath: z.string(), + interval: z.number(), + lastRotatedAt: z.date().nullable().optional(), + status: z.string().nullable().optional(), + statusMessage: z.string().nullable().optional(), + encryptedData: z.string().nullable().optional(), + encryptedDataIV: z.string().nullable().optional(), + encryptedDataTag: z.string().nullable().optional(), + algorithm: z.string().nullable().optional(), + keyEncoding: z.string().nullable().optional(), + envId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +export type TSecretRotations = z.infer; +export type TSecretRotationsInsert = Omit; +export type TSecretRotationsUpdate = Partial>; diff --git a/backend-pg/src/ee/routes/v1/index.ts b/backend-pg/src/ee/routes/v1/index.ts index 851e42614..5944382f5 100644 --- a/backend-pg/src/ee/routes/v1/index.ts +++ b/backend-pg/src/ee/routes/v1/index.ts @@ -1,10 +1,20 @@ import { registerOrgRoleRouter } from "./org-role-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router"; +import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router"; +import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; +import { registerSecretRotationRouter } from "./secret-rotation-router"; export const registerV1EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization await server.register(registerOrgRoleRouter, { prefix: "/organization" }); await server.register(registerProjectRoleRouter, { prefix: "/workspace" }); await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" }); + await server.register(registerSecretApprovalRequestRouter, { + prefix: "/secret-approval-requests" + }); + await server.register(registerSecretRotationProviderRouter, { + prefix: "/secret-rotation-providers" + }); + await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); }; diff --git a/backend-pg/src/ee/routes/v1/secret-approval-policy-router.ts b/backend-pg/src/ee/routes/v1/secret-approval-policy-router.ts index 7c473a80a..bef25c249 100644 --- a/backend-pg/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend-pg/src/ee/routes/v1/secret-approval-policy-router.ts @@ -1,8 +1,9 @@ -import { z } from "zod"; import { nanoid } from "nanoid"; +import { z } from "zod"; + import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; +import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvider) => { server.route({ diff --git a/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts b/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts new file mode 100644 index 000000000..71fe9fb41 --- /dev/null +++ b/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts @@ -0,0 +1,236 @@ +import { z } from "zod"; + +import { + SaRequestSecretsSchema, + SarReviewersSchema, + SecretApprovalRequestsSchema, + SecretsSchema, + SecretVersionsSchema +} from "@app/db/schemas"; +import { + ApprovalStatus, + RequestState +} from "@app/ee/services/secret-approval-request/secret-approval-request-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim().optional(), + committer: z.string().trim().optional(), + status: z.nativeEnum(RequestState).optional(), + limit: z.coerce.number().default(20), + offset: z.coerce.number().default(0) + }), + response: { + 200: z.object({ + approvals: SecretApprovalRequestsSchema.merge( + z.object({ + // secretPath: z.string(), + policy: z.object({ + id: z.string(), + name: z.string(), + approvals: z.number(), + approvers: z.string().array(), + secretPath: z.string().optional().nullable() + }), + reviewers: z.object({ member: z.string(), status: z.string() }).array(), + approvers: z.string().array() + }) + ).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approvals = await server.services.secretApprovalRequest.getSecretApprovals({ + actor: req.permission.type, + actorId: req.permission.id, + ...req.query, + projectId: req.query.workspaceId + }); + return { approvals }; + } + }); + + server.route({ + url: "/count", + method: "GET", + schema: { + querystring: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + approvals: z.object({ + open: z.number().default(0), + closed: z.number().default(0) + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approvals = await server.services.secretApprovalRequest.requestCount({ + actor: req.permission.type, + actorId: req.permission.id, + projectId: req.query.workspaceId + }); + return { approvals }; + } + }); + + server.route({ + url: "/:id", + method: "GET", + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + approval: SecretApprovalRequestsSchema.merge( + z.object({ + // secretPath: z.string(), + policy: z.object({ + id: z.string(), + name: z.string(), + approvals: z.number(), + approvers: z.string().array(), + secretPath: z.string().optional().nullable() + }), + reviewers: z.object({ member: z.string(), status: z.string() }).array(), + approvers: z.string().array(), + commits: SaRequestSecretsSchema.omit({ secretBlindIndex: true }) + .merge( + z.object({ + secret: SecretsSchema.pick({ + id: true, + secretKeyIV: true, + secretKeyTag: true, + secretKeyCiphertext: true, + secretValueIV: true, + secretValueTag: true, + secretValueCiphertext: true, + secretCommentIV: true, + secretCommentTag: true, + secretCommentCiphertext: true + }) + .optional() + .nullable(), + secretVersion: SecretVersionsSchema.pick({ + id: true, + secretKeyIV: true, + secretKeyTag: true, + secretKeyCiphertext: true, + secretValueIV: true, + secretValueTag: true, + secretValueCiphertext: true, + secretCommentIV: true, + secretCommentTag: true, + secretCommentCiphertext: true + }).optional() + }) + ) + .array() + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({ + actor: req.permission.type, + actorId: req.permission.id, + id: req.params.id + }); + return { approval }; + } + }); + + server.route({ + url: "/:id/merge", + method: "POST", + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + approval: SecretApprovalRequestsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + approvalId: req.params.id + }); + return { approval }; + } + }); + + server.route({ + url: "/:id/review", + method: "POST", + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + }), + response: { + 200: z.object({ + review: SarReviewersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const review = await server.services.secretApprovalRequest.reviewApproval({ + actorId: req.permission.id, + actor: req.permission.type, + approvalId: req.params.id, + status: req.body.status + }); + return { review }; + } + }); + + server.route({ + url: "/:id/status", + method: "POST", + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + status: z.nativeEnum(RequestState) + }), + response: { + 200: z.object({ + approval: SecretApprovalRequestsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approval = await server.services.secretApprovalRequest.updateApprovalStatus({ + actorId: req.permission.id, + actor: req.permission.type, + approvalId: req.params.id, + status: req.body.status + }); + return { approval }; + } + }); +}; diff --git a/backend-pg/src/ee/routes/v1/secret-rotation-provider-router.ts b/backend-pg/src/ee/routes/v1/secret-rotation-provider-router.ts new file mode 100644 index 000000000..bcaf1ab39 --- /dev/null +++ b/backend-pg/src/ee/routes/v1/secret-rotation-provider-router.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerSecretRotationProviderRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/:workspaceId", + method: "GET", + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + providers: z + .object({ + name: z.string(), + title: z.string(), + image: z.string().optional(), + description: z.string().optional(), + template: z.any() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const providers = await server.services.secretRotation.getProviderTemplates({ + actor: req.permission.type, + actorId: req.permission.id, + projectId: req.params.workspaceId + }); + return providers; + } + }); +}; diff --git a/backend-pg/src/ee/routes/v1/secret-rotation-router.ts b/backend-pg/src/ee/routes/v1/secret-rotation-router.ts new file mode 100644 index 000000000..95eb0bc88 --- /dev/null +++ b/backend-pg/src/ee/routes/v1/secret-rotation-router.ts @@ -0,0 +1,162 @@ +import { z } from "zod"; + +import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerSecretRotationRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z.object({ + workspaceId: z.string().trim(), + secretPath: z.string().trim(), + environment: z.string().trim(), + interval: z.number().min(1), + provider: z.string().trim(), + customProvider: z.string().trim().optional(), + inputs: z.record(z.unknown()), + outputs: z.record(z.string()) + }), + response: { + 200: z.object({ + secretRotation: SecretRotationsSchema.merge( + z.object({ + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }), + outputs: SecretRotationOutputsSchema.array() + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretRotation = await server.services.secretRotation.createRotation({ + actor: req.permission.type, + actorId: req.permission.id, + ...req.body, + projectId: req.body.workspaceId + }); + return { secretRotation }; + } + }); + + server.route({ + url: "/restart", + method: "POST", + schema: { + body: z.object({ + id: z.string().trim() + }), + response: { + 200: z.object({ + secretRotation: SecretRotationsSchema.merge( + z.object({ + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }) + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretRotation = await server.services.secretRotation.restartById({ + actor: req.permission.type, + actorId: req.permission.id, + rotationId: req.body.id + }); + return { secretRotation }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + secretRotations: SecretRotationsSchema.merge( + z.object({ + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }), + outputs: z + .object({ + key: z.string(), + secret: SecretsSchema.pick({ + id: true, + version: true, + secretKeyIV: true, + secretKeyTag: true, + secretKeyCiphertext: true, + secretValueIV: true, + secretValueTag: true, + secretValueCiphertext: true, + secretCommentIV: true, + secretCommentTag: true, + secretCommentCiphertext: true + }) + }) + .array() + }) + ).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretRotations = await server.services.secretRotation.getByProjectId({ + actor: req.permission.type, + actorId: req.permission.id, + projectId: req.query.workspaceId + }); + return { secretRotations }; + } + }); + + server.route({ + url: "/:id", + method: "DELETE", + schema: { + params: z.object({ + id: z.string().trim() + }), + response: { + 200: z.object({ + secretRotation: SecretRotationsSchema.merge( + z.object({ + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }) + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretRotation = await server.services.secretRotation.deleteById({ + actor: req.permission.type, + actorId: req.permission.id, + rotationId: req.params.id + }); + return { secretRotation }; + } + }); +}; diff --git a/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index 22183ceb6..75c0d7909 100644 --- a/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -1,14 +1,14 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TSecretApprovalPolicies, TableName } from "@app/db/schemas"; +import { TableName,TSecretApprovalPolicies } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { - TFindFilter, buildFindFilter, mergeOneToManyRelation, ormify, - selectAllTableCols -} from "@app/lib/knex"; -import { Knex } from "knex"; + selectAllTableCols, + TFindFilter} from "@app/lib/knex"; export type TSecretApprovalPolicyDalFactory = ReturnType; diff --git a/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index b96afc996..a8d6c276c 100644 --- a/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend-pg/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -1,6 +1,18 @@ +import { ForbiddenError } from "@casl/ability"; +import picomatch from "picomatch"; + import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { TSecretApprovalPolicyDalFactory } from "./secret-approval-policy-dal"; +import { + ProjectPermissionActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError } from "@app/lib/errors"; +import { containsGlobPatterns } from "@app/lib/picomatch"; +import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal"; +import { TProjectMembershipDalFactory } from "@app/services/project-membership/project-membership-dal"; + import { TSapApproverDalFactory } from "./sap-approver-dal"; +import { TSecretApprovalPolicyDalFactory } from "./secret-approval-policy-dal"; import { TCreateSapDTO, TDeleteSapDTO, @@ -8,16 +20,6 @@ import { TListSapDTO, TUpdateSapDTO } from "./secret-approval-policy-types"; -import { ForbiddenError } from "@casl/ability"; -import { - ProjectPermissionActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; -import { BadRequestError } from "@app/lib/errors"; -import picomatch from "picomatch"; -import { containsGlobPatterns } from "@app/lib/picomatch"; -import { TProjectMembershipDalFactory } from "@app/services/project-membership/project-membership-dal"; -import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal"; const getPolicyScore = (policy: { secretPath?: string | null }) => // if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2 diff --git a/backend-pg/src/ee/services/secret-approval-request/sar-secret-dal.ts b/backend-pg/src/ee/services/secret-approval-request/sar-secret-dal.ts index de86a9902..35dd2b5e0 100644 --- a/backend-pg/src/ee/services/secret-approval-request/sar-secret-dal.ts +++ b/backend-pg/src/ee/services/secret-approval-request/sar-secret-dal.ts @@ -1,25 +1,121 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; -import { Knex } from "knex"; export type TSarSecretDalFactory = ReturnType; export const sarSecretDalFactory = (db: TDbClient) => { const sarSecretOrm = ormify(db, TableName.SarSecret); - const findByRequestId = (requestId: string, tx?: Knex) => { + const findByRequestId = async (requestId: string, tx?: Knex) => { try { - const doc = (tx || db)(TableName.SarSecret) + const doc = await (tx || db)(TableName.SarSecret) .where({ requestId }) .leftJoin(TableName.Secret, `${TableName.SarSecret}.secretId`, `${TableName.Secret}.id`) + .leftJoin( + TableName.SecretVersion, + `${TableName.SecretVersion}.id`, + `${TableName.SarSecret}.secretVersion` + ) .select(selectAllTableCols(TableName.SarSecret)) .select( - db.ref("secretBlindIndex").withSchema(TableName.Secret).as("latestSecretBlindIndex"), - db.ref("version").withSchema(TableName.Secret).as("latestSecretVersion") + db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), + db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), + db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), + db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), + db + .ref("secretCommentCiphertext") + .withSchema(TableName.Secret) + .as("orgSecCommentCiphertext") + ) + .select( + // db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindInex"), + db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), + db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), + db + .ref("secretKeyCiphertext") + .withSchema(TableName.SecretVersion) + .as("secVerKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), + db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), + db + .ref("secretValueCiphertext") + .withSchema(TableName.SecretVersion) + .as("secVerValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), + db + .ref("secretCommentCiphertext") + .withSchema(TableName.SecretVersion) + .as("secVerCommentCiphertext") ); - return doc; + return doc.map( + ({ + orgSecKeyIV, + orgSecKeyTag, + orgSecValueIV, + orgSecVersion, + orgSecValueTag, + orgSecCommentIV, + orgSecBlindIndex, + orgSecCommentTag, + orgSecKeyCiphertext, + orgSecValueCiphertext, + orgSecCommentCiphertext, + secVerCommentIV, + secVerCommentCiphertext, + secVerCommentTag, + secVerValueCiphertext, + secVerKeyIV, + secVerKeyTag, + secVerValueIV, + secVerVersion, + secVerValueTag, + secVerKeyCiphertext, + ...el + }) => ({ + ...el, + secret: el.secretId + ? { + id: el.secretId, + secretBlindIndex: orgSecBlindIndex, + secretKeyIV: orgSecKeyIV, + secretKeyTag: orgSecKeyTag, + secretKeyCiphertext: orgSecKeyCiphertext, + secretValueIV: orgSecValueIV, + secretValueTag: orgSecValueTag, + secretValueCiphertext: orgSecValueCiphertext, + secretCommentIV: orgSecCommentIV, + secretCommentTag: orgSecCommentTag, + secretCommentCiphertext: orgSecCommentCiphertext + } + : undefined, + secretVersion: el.secretVersion + ? { + id: el.secretVersion, + secretKeyIV: secVerKeyIV, + secretKeyTag: secVerKeyTag, + secretKeyCiphertext: secVerKeyCiphertext, + secretValueIV: secVerValueIV, + secretValueTag: secVerValueTag, + secretValueCiphertext: secVerValueCiphertext, + secretCommentIV: secVerCommentIV, + secretCommentTag: secVerCommentTag, + secretCommentCiphertext: secVerCommentCiphertext + } + : undefined + }) + ); } catch (error) { throw new DatabaseError({ error, name: "FindByRequestId" }); } diff --git a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 2fd193949..b5723bd60 100644 --- a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -1,8 +1,16 @@ -import { TDbClient } from "@app/db"; -import { TSecretApprovalRequests, TableName } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; -import { TFindFilter, ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { SecretApprovalRequestsSchema, TableName, TSecretApprovalRequests } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { + ormify, + selectAllTableCols, + sqlNestRelationships, + stripUndefinedInWhere, + TFindFilter +} from "@app/lib/knex"; + import { RequestState } from "./secret-approval-request-types"; export type TSecretApprovalRequestDalFactory = ReturnType; @@ -23,27 +31,33 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { const findQuery = (filter: TFindFilter, tx: Knex) => tx(TableName.SecretApprovalRequest) .where(filter) + .join( + TableName.SecretFolder, + `${TableName.SecretApprovalRequest}.folderId`, + `${TableName.SecretFolder}.id` + ) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join( TableName.SecretApprovalPolicy, `${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalPolicy}.id` ) - .join( - TableName.SarReviewer, - `${TableName.SecretApprovalRequest}.id`, - `${TableName.SarReviewer}.requestId` - ) .join( TableName.SapApprover, `${TableName.SecretApprovalPolicy}.id`, `${TableName.SapApprover}.policyId` ) + .leftJoin( + TableName.SarReviewer, + `${TableName.SecretApprovalRequest}.id`, + `${TableName.SarReviewer}.requestId` + ) .select(selectAllTableCols(TableName.SecretApprovalRequest)) - .select(tx.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId")) - .select(tx.ref("statue").withSchema(TableName.SarReviewer).as("reviewerStatus")) + .select(tx.ref("member").withSchema(TableName.SarReviewer).as("reviewerMemberId")) + .select(tx.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus")) .select(tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId")) .select(tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName")) - .select(tx.ref("projectId").withSchema(TableName.SecretApprovalPolicy)) + .select(tx.ref("projectId").withSchema(TableName.Environment)) .select( tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath") ) @@ -52,51 +66,27 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const docs = await findQuery({ id }, tx || db); + const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db); + const docs = await sql; const formatedDoc = sqlNestRelationships({ data: docs, key: "id", - parentMapper: ({ - id: pk, - projectId, - hasMerged, - status, - conflicts, - slug, - folderId, - statusChangeBy, - committerId, - createdAt, - updatedAt, - policyId, - policyName, - policyApprovals, - policySecretPath - }) => ({ - id: pk, - hasMerged, - projectId, - status, - conflicts, - slug, - folderId, - statusChangeBy, - committerId, - createdAt, - updatedAt, - policyId, + parentMapper: (el) => ({ + ...SecretApprovalRequestsSchema.parse(el), + projectId: el.projectId, policy: { - id: policyId, - name: policyName, - approvals: policyApprovals, - secretPath: policySecretPath + id: el.policyId, + name: el.policyName, + approvals: el.policyApprovals, + secretPath: el.policySecretPath } }), childrenMapper: [ { key: "reviewerMemberId", label: "reviewers", - mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => ({ member, status }) + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => + member ? { member, status } : undefined }, { key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId } ] as const @@ -124,18 +114,27 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id` ) - .where({ projectId }) .join( TableName.SapApprover, `${TableName.SecretApprovalRequest}.policyId`, `${TableName.SapApprover}.policyId` ) + .where({ projectId }) .where(`${TableName.SapApprover}.approverId`, membershipId) .orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId) .groupBy("status") - .count("status"); - console.log(JSON.stringify(doc, null, 4)); - return { open: 0, closed: 0 }; + .count("status") + .select("status"); + return { + open: parseInt( + (doc.find(({ status }) => status === RequestState.Open)?.count as string) || "0", + 10 + ), + closed: parseInt( + (doc.find(({ status }) => status === RequestState.Closed)?.count as string) || "0", + 10 + ) + }; } catch (error) { throw new DatabaseError({ error, name: "FindRequestCount" }); } @@ -157,7 +156,6 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id` ) - .where({ projectId, slug: environment, status, committerId: committer }) .join( TableName.SecretApprovalPolicy, `${TableName.SecretApprovalRequest}.policyId`, @@ -168,11 +166,28 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { `${TableName.SecretApprovalPolicy}.id`, `${TableName.SapApprover}.policyId` ) - .where(`${TableName.SapApprover}.approverId`, membershipId) - .orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId) + .leftJoin( + TableName.SarReviewer, + `${TableName.SecretApprovalRequest}.id`, + `${TableName.SarReviewer}.requestId` + ) + .where( + stripUndefinedInWhere({ + projectId, + slug: environment, + [`${TableName.SecretApprovalRequest}.status`]: status, + committerId: committer + }) + ) + .andWhere((bd) => + bd + .where(`${TableName.SapApprover}.approverId`, membershipId) + .orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId) + ) .select(selectAllTableCols(TableName.SecretApprovalRequest)) + .select(db.ref("projectId").withSchema(TableName.Environment)) .select(db.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId")) - .select(db.ref("statue").withSchema(TableName.SarReviewer).as("reviewerStatus")) + .select(db.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus")) .select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId")) .select(db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName")) .select( @@ -185,46 +200,22 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { const formatedDoc = sqlNestRelationships({ data: docs, key: "id", - parentMapper: ({ - id: pk, - hasMerged, - status: pStatus, - conflicts, - slug, - folderId, - statusChangeBy, - committerId, - createdAt, - updatedAt, - policyId, - policyName, - policyApprovals, - policySecretPath - }) => ({ - id: pk, - hasMerged, - projectId, - status: pStatus, - conflicts, - slug, - folderId, - statusChangeBy, - committerId, - createdAt, - updatedAt, - policyId, + parentMapper: (el) => ({ + ...SecretApprovalRequestsSchema.parse(el), + projectId: el.projectId, policy: { - id: policyId, - name: policyName, - approvals: policyApprovals, - secretPath: policySecretPath + id: el.policyId, + name: el.policyName, + approvals: el.policyApprovals, + secretPath: el.policySecretPath } }), childrenMapper: [ { key: "reviewerMemberId", label: "reviewers", - mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => ({ member, status: s }) + mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => + member ? { member, status: s } : undefined }, { key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId } ] as const diff --git a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts index a2c4caabe..e589ecd54 100644 --- a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -1,19 +1,5 @@ -import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal"; -import { TSecretApprovalRequestDalFactory } from "./secret-approval-request-dal"; -import { - ApprovalStatus, - CommitType, - TApprovalRequestCountDTO, - TGenerateSecretApprovalRequestDTO, - TListApprovalsDTO, - TMergeSecretApprovalRequestDTO, - TReviewRequestDTO, - TSecretApprovalDetailsDTO, - TStatusChangeDTO -} from "./secret-approval-request-types"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; -import { TSecretBlindIndexDalFactory } from "@app/services/secret/secret-blind-index-dal"; -import { generateSecretBlindIndexBySalt } from "@app/services/secret/secret-service"; +import { ForbiddenError, subject } from "@casl/ability"; + import { ProjectMembershipRole, SecretEncryptionAlgo, @@ -22,13 +8,32 @@ import { TSaRequestSecretsInsert, TSecrets } from "@app/db/schemas"; -import { TSecretDalFactory } from "@app/services/secret/secret-dal"; -import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { TSarSecretDalFactory } from "./sar-secret-dal"; import { ActorType } from "@app/services/auth/auth-type"; +import { TSecretBlindIndexDalFactory } from "@app/services/secret/secret-blind-index-dal"; +import { TSecretDalFactory } from "@app/services/secret/secret-dal"; +import { generateSecretBlindIndexBySalt } from "@app/services/secret/secret-service"; +import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal"; +import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal"; + import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSarReviewerDalFactory } from "./sar-reviewer-dal"; +import { TSarSecretDalFactory } from "./sar-secret-dal"; +import { TSecretApprovalRequestDalFactory } from "./secret-approval-request-dal"; +import { + ApprovalStatus, + CommitType, + RequestState, + TApprovalRequestCountDTO, + TGenerateSecretApprovalRequestDTO, + TListApprovalsDTO, + TMergeSecretApprovalRequestDTO, + TReviewRequestDTO, + TSecretApprovalDetailsDTO, + TStatusChangeDTO +} from "./secret-approval-request-types"; type TSecretApprovalRequestServiceFactoryDep = { permissionService: Pick; @@ -167,9 +172,9 @@ export const secretApprovalRequestServiceFactory = ({ if (secretApprovalRequest.hasMerged) throw new BadRequestError({ message: "Approval request has been merged" }); - if (secretApprovalRequest.status === "close" && status === "close") + if (secretApprovalRequest.status === RequestState.Closed && status === RequestState.Closed) throw new BadRequestError({ message: "Approval request is already closed" }); - if (secretApprovalRequest.status === "open" && status === "open") + if (secretApprovalRequest.status === RequestState.Open && status === RequestState.Open) throw new BadRequestError({ message: "Approval request is already open" }); const updatedRequest = await secretApprovalRequestDal.updateById(secretApprovalRequest.id, { @@ -203,7 +208,7 @@ export const secretApprovalRequestServiceFactory = ({ throw new UnauthorizedError({ message: "User has no access" }); } const reviewers = secretApprovalRequest.reviewers.reduce>( - (prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }), + (prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status as ApprovalStatus }), {} ); const hasMinApproval = @@ -245,10 +250,14 @@ export const secretApprovalRequestServiceFactory = ({ if (secretUpdationCommits.length) { const conflictedByNewBlindIndex = await secretDal.findByBlindIndexes( folderId, - secretUpdationCommits.map(({ secretBlindIndex }) => ({ - type: SecretType.Shared, - blindIndex: secretBlindIndex - })) + secretUpdationCommits + .filter( + ({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex + ) + .map(({ secretBlindIndex }) => ({ + type: SecretType.Shared, + blindIndex: secretBlindIndex + })) ); const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce>( (prev, curr) => @@ -276,118 +285,128 @@ export const secretApprovalRequestServiceFactory = ({ ); const mergeStatus = await secretDal.transaction(async (tx) => { - const newSecrets = await secretDal.insertMany( - secretCreationCommits.map( - ({ - secretBlindIndex, - metadata, - secretKeyIV, - secretKeyTag, - secretKeyCiphertext, - secretValueIV, - secretValueTag, - secretValueCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentCiphertext, - skipMultilineEncoding, - secretReminderNotice, - secretReminderRepeatDays - }) => ({ - secretBlindIndex, - metadata, - secretKeyIV, - secretKeyTag, - secretKeyCiphertext, - secretValueIV, - secretValueTag, - secretValueCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentCiphertext, - skipMultilineEncoding, - secretReminderNotice, - secretReminderRepeatDays, - version: 1, + const newSecrets = secretCreationCommits.length + ? await secretDal.insertMany( + secretCreationCommits.map( + ({ + secretBlindIndex, + metadata, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueIV, + secretValueTag, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretReminderNotice, + secretReminderRepeatDays + }) => ({ + secretBlindIndex, + metadata, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueIV, + secretValueTag, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretReminderNotice, + secretReminderRepeatDays, + version: 1, + folderId, + type: SecretType.Shared, + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.UTF8 + }) + ), + tx + ) + : []; + const updatedSecrets = secretUpdationCommits.length + ? await secretDal.bulkUpdate( + secretUpdationCommits.map( + ({ + version, + secretId, + secretBlindIndex, + metadata, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueIV, + secretValueTag, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretReminderNotice, + secretReminderRepeatDays + }) => ({ + folderId, + version: (version || 0) + 1, + id: secretId as string, + type: SecretType.Shared, + secretBlindIndex, + metadata, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueIV, + secretValueTag, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretReminderNotice, + secretReminderRepeatDays + }) + ), + tx + ) + : []; + const deletedSecret = secretDeletionCommits.length + ? await secretDal.deleteMany( + secretDeletionCommits.map(({ secretBlindIndex }) => ({ + blindIndex: secretBlindIndex, + type: SecretType.Shared + })), folderId, - type: SecretType.Shared, - algorithm: SecretEncryptionAlgo.AES_256_GCM, - keyEncoding: SecretKeyEncoding.UTF8 - }) - ), - tx - ); - const updatedSecrets = await secretDal.bulkUpdate( - secretUpdationCommits.map( - ({ - secretId, - secretBlindIndex, - metadata, - secretKeyIV, - secretKeyTag, - secretKeyCiphertext, - secretValueIV, - secretValueTag, - secretValueCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentCiphertext, - skipMultilineEncoding, - secretReminderNotice, - secretReminderRepeatDays - }) => ({ - folderId, - id: secretId as string, - type: SecretType.Shared, - secretBlindIndex, - metadata, - secretKeyIV, - secretKeyTag, - secretKeyCiphertext, - secretValueIV, - secretValueTag, - secretValueCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentCiphertext, - skipMultilineEncoding, - secretReminderNotice, - secretReminderRepeatDays - }) - ), - tx - ); - const deletedSecret = await secretDal.deleteMany( - secretDeletionCommits.map(({ secretBlindIndex }) => ({ - blindIndex: secretBlindIndex, - type: SecretType.Shared - })), - folderId, - actorId, - tx - ); - await secretVersionDal.insertMany( - newSecrets - .map(({ id, updatedAt, createdAt, ...el }) => ({ - ...el, - secretId: id - })) - .concat( - updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ + actorId, + tx + ) + : []; + if (newSecrets.length || updatedSecrets.length) { + await secretVersionDal.insertMany( + newSecrets + .map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })) - ), - tx - ); + .concat( + updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ + ...el, + secretId: id + })) + ), + tx + ); + } const updatedSecretApproval = await secretApprovalRequestDal.updateById( secretApprovalRequest.id, { - conflicts, + conflicts: JSON.stringify(conflicts), hasMerged: true, - status: "close", - statusChangeBy: actorId + status: RequestState.Closed, + statusChangeBy: membership.id }, tx ); @@ -403,12 +422,23 @@ export const secretApprovalRequestServiceFactory = ({ // this will keep a copy to do merge later when accepting const generateSecretApprovalRequest = async ({ data, + actorId, + actor, policy, projectId, secretPath, - environment, - commiterMembershipId + environment }: TGenerateSecretApprovalRequestDTO) => { + const { permission, membership } = await permissionService.getProjectPermission( + actor, + actorId, + projectId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + const folder = await folderDal.findBySecretPath(projectId, environment, secretPath); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" }); @@ -449,7 +479,9 @@ export const secretApprovalRequestServiceFactory = ({ ...el, op: CommitType.Create as const, version: 0, - secretBlindIndex: secretBlindIndexes[el.secretName] + secretBlindIndex: secretBlindIndexes[el.secretName], + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.BASE64 })) ); } @@ -533,7 +565,8 @@ export const secretApprovalRequestServiceFactory = ({ secret: secretId, secretVersion: latestSecretVersions[secretId].id, ...el, - secretBlindIndex: newSecretBlindIndexes?.[el.secretName], + secretBlindIndex: + newSecretBlindIndexes?.[el.secretName] || secretBlindIndexes[el.secretName], version: secretsGroupedByBlindIndex[secretBlindIndexes[el.secretName]].version || 1 }; }) @@ -596,6 +629,7 @@ export const secretApprovalRequestServiceFactory = ({ ); } + if (!commits.length) throw new BadRequestError({ message: "Empty commits" }); const secretApprovalRequest = await secretApprovalRequestDal.transaction(async (tx) => { const doc = await secretApprovalRequestDal.create( { @@ -604,7 +638,7 @@ export const secretApprovalRequestServiceFactory = ({ policyId: policy.id, status: "open", hasMerged: false, - committerId: commiterMembershipId + committerId: membership.id }, tx ); diff --git a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-types.ts index 43de12008..4ea70b2aa 100644 --- a/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-types.ts +++ b/backend-pg/src/ee/services/secret-approval-request/secret-approval-request-types.ts @@ -9,7 +9,7 @@ export enum CommitType { export enum RequestState { Open = "open", - Closed = "closed" + Closed = "close" } export enum ApprovalStatus { @@ -18,28 +18,36 @@ export enum ApprovalStatus { REJECTED = "rejected" } -type TApprovalCreateSecret = Omit & { +type TApprovalCreateSecret = Omit< + TSaRequestSecrets, + | TImmutableDBKeys + | "version" + | "algorithm" + | "keyEncoding" + | "requestId" + | "op" + | "secretVersion" + | "secretBlindIndex" +> & { secretName: string; tagIds?: string[]; }; -type TApprovalUpdateSecret = Partial> & { +type TApprovalUpdateSecret = Partial & { secretName: string; newSecretName?: string; tagIds?: string[]; }; export type TGenerateSecretApprovalRequestDTO = { - projectId: string; environment: string; secretPath: string; policy: TSecretApprovalPolicies; - commiterMembershipId: string; data: { - [CommitType.Create]: TApprovalCreateSecret[]; - [CommitType.Update]: TApprovalUpdateSecret[]; - [CommitType.Delete]: { secretName: string }[]; + [CommitType.Create]?: TApprovalCreateSecret[]; + [CommitType.Update]?: TApprovalUpdateSecret[]; + [CommitType.Delete]?: { secretName: string }[]; }; -}; +} & TProjectPermission; export type TMergeSecretApprovalRequestDTO = { approvalId: string; @@ -47,7 +55,7 @@ export type TMergeSecretApprovalRequestDTO = { export type TStatusChangeDTO = { approvalId: string; - status: "open" | "close"; + status: RequestState; } & Omit; export type TReviewRequestDTO = { @@ -67,5 +75,5 @@ export type TListApprovalsDTO = { } & TProjectPermission; export type TSecretApprovalDetailsDTO = { - id:string; -} & Omit + id: string; +} & Omit; diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-dal.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-dal.ts new file mode 100644 index 000000000..5f59fdbfd --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-dal.ts @@ -0,0 +1,138 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { SecretRotationsSchema, TableName, TSecretRotations } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; + +export type TSecretRotationDalFactory = ReturnType; + +export const secretRotationDalFactory = (db: TDbClient) => { + const secretRotationOrm = ormify(db, TableName.SecretRotation); + const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput); + + const findQuery = (filter: TFindFilter, tx: Knex) => + tx(TableName.SecretRotation) + .where(filter) + .join( + TableName.Environment, + `${TableName.SecretRotation}.envId`, + `${TableName.Environment}.id` + ) + .leftJoin( + TableName.SecretRotationOutput, + `${TableName.SecretRotation}.id`, + `${TableName.SecretRotationOutput}.rotationId` + ) + .join( + TableName.Secret, + `${TableName.SecretRotationOutput}.secretId`, + `${TableName.Secret}.id` + ) + .select(selectAllTableCols(TableName.SecretRotation)) + .select(tx.ref("name").withSchema(TableName.Environment).as("envName")) + .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) + .select(tx.ref("id").withSchema(TableName.Environment).as("envId")) + .select(tx.ref("projectId").withSchema(TableName.Environment)) + .select(tx.ref("key").withSchema(TableName.SecretRotationOutput).as("outputKey")) + .select(tx.ref("id").withSchema(TableName.Secret).as("secId")) + .select(tx.ref("version").withSchema(TableName.Secret).as("secVersion")) + .select(tx.ref("secretKeyIV").withSchema(TableName.Secret)) + .select(tx.ref("secretKeyTag").withSchema(TableName.Secret)) + .select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret)) + .select(tx.ref("secretValueIV").withSchema(TableName.Secret)) + .select(tx.ref("secretValueTag").withSchema(TableName.Secret)) + .select(tx.ref("secretValueCiphertext").withSchema(TableName.Secret)) + .select(tx.ref("secretCommentIV").withSchema(TableName.Secret)) + .select(tx.ref("secretCommentTag").withSchema(TableName.Secret)) + .select(tx.ref("secretCommentCiphertext").withSchema(TableName.Secret)); + + const find = async (filter: TFindFilter, tx?: Knex) => { + try { + const data = await findQuery(filter, tx || db); + return sqlNestRelationships({ + data, + key: "id", + parentMapper: (el) => ({ + ...SecretRotationsSchema.parse(el), + projectId: el.projectId, + environment: { id: el.envId, name: el.envName, slug: el.envSlug } + }), + childrenMapper: [ + { + key: "secId", + label: "outputs", + mapper: ({ + secId, + outputKey, + secVersion, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueTag, + secretValueIV, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext + }) => ({ + key: outputKey, + secret: { + id: secId, + version: secVersion, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueTag, + secretValueIV, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext + } + }) + } + ] as const + }); + } catch (error) { + throw new DatabaseError({ error, name: "SecretRotationFind" }); + } + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const doc = await (tx || db)(TableName.SecretRotation) + .join( + TableName.Environment, + `${TableName.SecretRotation}.envId`, + `${TableName.Environment}.id` + ) + .where({ [`${TableName.SecretRotation}.id` as "id"]: id }) + .select(selectAllTableCols(TableName.SecretRotation)) + .select( + db.ref("id").withSchema(TableName.Environment).as("envId"), + db.ref("projectId").withSchema(TableName.Environment), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + .first(); + if (doc) { + const { envName, envSlug, envId, ...el } = doc; + return { ...el, envId, environment: { id: envId, slug: envSlug, name: envName } }; + } + } catch (error) { + throw new DatabaseError({ error, name: "SecretRotationFindById" }); + } + }; + + const findRotationOutputsByRotationId = async (rotationId: string) => + secretRotationOutputOrm.find({ rotationId }); + + return { + ...secretRotationOrm, + find, + findById, + secretOutputInsertMany: secretRotationOutputOrm.insertMany, + findRotationOutputsByRotationId + }; +}; diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/index.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/index.ts new file mode 100644 index 000000000..aea9a206a --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/index.ts @@ -0,0 +1,5 @@ +export { + DisableRotationErrors, + secretRotationQueueFactory, + TSecretRotationQueueFactory +} from "./secret-rotation-queue"; diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts new file mode 100644 index 000000000..0ffded964 --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts @@ -0,0 +1,157 @@ +/* eslint-disable no-param-reassign */ +import axios from "axios"; +import jmespath from "jmespath"; +import knex from "knex"; + +import { getConfig } from "@app/lib/config/env"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { + TAssignOp, + TDbProviderClients, + TDirectAssignOp, + THttpProviderFunction +} from "../templates/types"; +import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types"; + +const REGEX = /\${([^}]+)}/g; +const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; + +export const interpolate = (data: any, getValue: (key: string) => unknown) => { + if (!data) return; + + if (typeof data === "number") return data; + + if (typeof data === "string") { + return data.replace(REGEX, (_a, b) => getValue(b) as string); + } + + if (typeof data === "object" && Array.isArray(data)) { + data.forEach((el, index) => { + // eslint-disable-next-line + data[index] = interpolate(el, getValue); + }); + } + + if (typeof data === "object") { + if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref); + const temp = data as Record; // for converting ts object to record type + Object.keys(temp).forEach((key) => { + temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue); + }); + } + return data; +}; + +const getInterpolationValue = (variables: TSecretRotationData) => (key: string) => { + if (key.includes("|")) { + const [keyword, ...arg] = key.split("|").map((el) => el.trim()); + switch (keyword) { + case "random": { + return alphaNumericNanoId(parseInt(arg[0], 10)); + } + default: { + throw Error(`Interpolation key not found - ${key}`); + } + } + } + const [type, keyName] = key.split(".").map((el) => el.trim()); + return variables[type as keyof TSecretRotationData][keyName]; +}; + +export const secretRotationHttpFn = async ( + func: THttpProviderFunction, + variables: TSecretRotationData +) => { + // string interpolation + const headers = interpolate(func.header, getInterpolationValue(variables)); + const url = interpolate(func.url, getInterpolationValue(variables)); + const body = interpolate(func.body, getInterpolationValue(variables)); + // axios will automatically throw error if req status is not between 2xx range + return axios({ + method: func.method, + url, + headers, + data: body, + timeout: EXTERNAL_REQUEST_TIMEOUT + }); +}; + +export const secretRotationDbFn = async ({ + ca, + host, + port, + query, + database, + password, + username, + client, + variables +}: TSecretRotationDbFn) => { + const appCfg = getConfig(); + + const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; + if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host)) + throw new Error("Invalid db host"); + + const db = knex({ + client, + connection: { + db: database, + port, + host, + user: username, + password, + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ssl, + pool: { min: 0, max: 1 } + } + }); + const data = await db.raw(query, variables); + return data; +}; + +export const secretRotationPreSetFn = ( + op: Record, + variables: TSecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + Object.entries(op || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof TSecretRotationData, string]; + variables[type][keyName] = interpolate(assignFn.value, getValFn); + }); +}; + +export const secretRotationHttpSetFn = async ( + func: THttpProviderFunction, + variables: TSecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + // http setter + const res = await secretRotationHttpFn(func, variables); + Object.entries(func.setter || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof TSecretRotationData, string]; + if (assignFn.assign === TAssignOp.JmesPath) { + variables[type][keyName] = jmespath.search(res.data, assignFn.path); + } else if (assignFn.value) { + variables[type][keyName] = interpolate(assignFn.value, getValFn); + } + }); +}; + +export const getDbSetQuery = ( + db: TDbProviderClients, + variable: { username: string; password: string } +) => { + if (db === TDbProviderClients.Pg) { + return { + query: "ALTER USER :username WITH PASSWORD :password", + variable + }; + } + // add more based on client + return { + query: "ALTER USER :username IDENTIFIED BY :password", + variable + }; +}; diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-types.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-types.ts new file mode 100644 index 000000000..b030b0f53 --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-types.ts @@ -0,0 +1,27 @@ +import { TDbProviderClients } from "../templates/types"; + +export type TSecretRotationEncData = { + inputs: Record; + creds: Array<{ + outputs: Record; + internal: Record; + }>; +}; + +export type TSecretRotationData = { + inputs: Record; + outputs: Record; + internal: Record; +}; + +export type TSecretRotationDbFn = { + client: TDbProviderClients; + username: string; + password: string; + host: string; + database: string; + port: number; + query: string; + variables: Record; + ca?: string; +}; diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts new file mode 100644 index 000000000..0a0effd9e --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -0,0 +1,245 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; +import { + encryptSymmetric128BitHexKeyUTF8, + infisicalSymmetricDecrypt, + infisicalSymmetricEncypt +} from "@app/lib/crypto/encryption"; +import { daysToMillisecond } from "@app/lib/dates"; +import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; +import { TSecretDalFactory } from "@app/services/secret/secret-dal"; +import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal"; + +import { TSecretRotationDalFactory } from "../secret-rotation-dal"; +import { rotationTemplates } from "../templates"; +import { TProviderFunctionTypes, TSecretRotationProviderTemplate } from "../templates/types"; +import { + getDbSetQuery, + secretRotationDbFn, + secretRotationHttpFn, + secretRotationHttpSetFn, + secretRotationPreSetFn} from "./secret-rotation-queue-fn"; +import { + TSecretRotationData, + TSecretRotationDbFn, + TSecretRotationEncData +} from "./secret-rotation-queue-types"; + +export type TSecretRotationQueueFactory = ReturnType; + +type TSecretRotationQueueFactoryDep = { + queue: TQueueServiceFactory; + secretRotationDal: TSecretRotationDalFactory; + projectBotService: Pick; + secretDal: Pick; + secretVersionDal: Pick; +}; + +// These error should stop the repeatable job and ask user to reconfigure rotation +export class DisableRotationErrors extends Error { + name: string; + + error: unknown; + + constructor({ name, error, message }: { message: string; name?: string; error?: unknown }) { + super(message); + this.name = name || "DisableRotationErrors"; + this.error = error; + } +} + +export const secretRotationQueueFactory = ({ + queue, + secretRotationDal, + projectBotService, + secretDal, + secretVersionDal +}: TSecretRotationQueueFactoryDep) => { + const addToQueue = async (rotationId: string, interval: number) => + queue.queue( + QueueName.SecretRotation, + QueueJobs.SecretRotation, + { rotationId }, + { jobId: rotationId, repeat: { every: daysToMillisecond(interval), immediately: true } } + ); + + const removeFromQueue = async (rotationId: string) => + queue.stopRepeatableJob(QueueName.SecretRotation, rotationId); + + queue.start(QueueName.SecretRotation, async (job) => { + const { rotationId } = job.data; + logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`); + const secretRotation = await secretRotationDal.findById(rotationId); + const rotationProvider = rotationTemplates.find( + ({ name }) => name === secretRotation?.provider + ); + + try { + if (!rotationProvider || !secretRotation) + throw new DisableRotationErrors({ message: "Provider not found" }); + + const rotationOutputs = await secretRotationDal.findRotationOutputsByRotationId(rotationId); + if (!rotationOutputs.length) + throw new DisableRotationErrors({ message: "Secrets not found" }); + + // deep copy + const provider = JSON.parse( + JSON.stringify(rotationProvider) + ) as TSecretRotationProviderTemplate; + + // now get the encrypted variable values + // in includes the inputs, the previous outputs + // internal mapping variables etc + const { encryptedDataTag, encryptedDataIV, encryptedData, keyEncoding } = secretRotation; + if (!encryptedDataTag || !encryptedDataIV || !encryptedData || !keyEncoding) { + throw new DisableRotationErrors({ message: "No inputs found" }); + } + const decryptedData = infisicalSymmetricDecrypt({ + keyEncoding: keyEncoding as SecretKeyEncoding, + ciphertext: encryptedData, + iv: encryptedDataIV, + tag: encryptedDataTag + }); + + const variables = JSON.parse(decryptedData) as TSecretRotationEncData; + // rotation set cycle + const newCredential: TSecretRotationData = { + inputs: variables.inputs, + outputs: {}, + internal: {} + }; + + // when its a database we keep cycling the variables accordingly + if (provider.template.type === TProviderFunctionTypes.DB) { + const lastCred = variables.creds.at(-1); + if (lastCred && variables.creds.length === 1) { + newCredential.internal.username = + lastCred.internal.username === variables.inputs.username1 + ? variables.inputs.username2 + : variables.inputs.username1; + } else { + newCredential.internal.username = lastCred + ? lastCred.internal.username + : variables.inputs.username1; + } + // set a random value for new password + newCredential.internal.rotated_password = alphaNumericNanoId(32); + const { username, password, host, database, port, ca } = newCredential.inputs; + const dbFunctionArg = { + username, + password, + host, + database, + port, + ca: ca as string, + client: provider.template.client + } as TSecretRotationDbFn; + // set function + await secretRotationDbFn({ + ...dbFunctionArg, + ...getDbSetQuery(provider.template.client, { + password: newCredential.internal.rotated_password as string, + username: newCredential.internal.username as string + }) + }); + // test function + await secretRotationDbFn({ + ...dbFunctionArg, + query: "SELECT NOW()", + variables: {} + }); + // clean up + if (variables.creds.length === 2) variables.creds.pop(); + } + + if (provider.template.type === TProviderFunctionTypes.HTTP) { + if (provider.template.functions.set?.pre) { + secretRotationPreSetFn(provider.template.functions.set.pre, newCredential); + } + await secretRotationHttpSetFn(provider.template.functions.set, newCredential); + // now test + await secretRotationHttpFn(provider.template.functions.test, newCredential); + if (variables.creds.length === 2) { + const deleteCycleCred = variables.creds.pop(); + if (deleteCycleCred && provider.template.functions.remove) { + const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred }; + if (provider.template.type === TProviderFunctionTypes.HTTP) { + await secretRotationHttpFn(provider.template.functions.remove, deleteCycleVar); + } + } + } + } + + variables.creds.unshift({ + outputs: newCredential.outputs, + internal: newCredential.internal + }); + const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables)); + const key = await projectBotService.getBotKey(secretRotation.projectId); + const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({ + secretId, + value: encryptSymmetric128BitHexKeyUTF8( + typeof newCredential.outputs[outputKey] === "object" + ? JSON.stringify(newCredential.outputs[outputKey]) + : String(newCredential.outputs[outputKey]), + key + ) + })); + await secretRotationDal.transaction(async (tx) => { + await secretRotationDal.updateById( + rotationId, + { + encryptedData: encVarData.ciphertext, + encryptedDataIV: encVarData.iv, + encryptedDataTag: encVarData.tag, + keyEncoding: encVarData.encoding, + algorithm: encVarData.algorithm, + lastRotatedAt: new Date(), + statusMessage: "Rotated successfull", + status: "success" + }, + tx + ); + const updatedSecrets = await secretDal.bulkUpdate( + encryptedSecrets.map(({ secretId, value }) => ({ + id: secretId, + secretValueCiphertext: value.ciphertext, + secretValueIV: value.iv, + secretValueTag: value.tag + })), + tx + ); + await secretDal.bulkUpdate( + updatedSecrets.map(({ id, version }) => ({ id, version: (version || 0) + 1 })), + tx + ); + await secretVersionDal.insertMany( + updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ + ...el, + secretId: id + })), + tx + ); + }); + } catch (error) { + if (error instanceof DisableRotationErrors) { + if (job.id) { + queue.stopRepeatableJob(QueueName.SecretRotation, job.id); + } + } + + await secretRotationDal.updateById(rotationId, { + status: "failed", + statusMessage: (error as Error).message.slice(0, 500), + lastRotatedAt: new Date() + }); + } + }); + + return { + addToQueue, + removeFromQueue + }; +}; diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-service.ts new file mode 100644 index 000000000..4a8f9bb01 --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -0,0 +1,195 @@ +import { ForbiddenError } from "@casl/ability"; +import Ajv from "ajv"; + +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { BadRequestError } from "@app/lib/errors"; +import { TProjectPermission } from "@app/lib/types"; +import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TSecretRotationDalFactory } from "./secret-rotation-dal"; +import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; +import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; +import { + TCreateSecretRotationDTO, + TDeleteDTO, + TGetByIdDTO, + TListByProjectIdDTO, + TRestartDTO +} from "./secret-rotation-types"; +import { rotationTemplates } from "./templates"; + +type TSecretRotationServiceFactoryDep = { + secretRotationDal: TSecretRotationDalFactory; + projectEnvDal: Pick; + permissionService: Pick; + secretRotationQueue: TSecretRotationQueueFactory; +}; + +export type TSecretRotationServiceFactory = ReturnType; + +const ajv = new Ajv({ strict: false }); +export const secretRotationServiceFactory = ({ + secretRotationDal, + permissionService, + projectEnvDal, + secretRotationQueue +}: TSecretRotationServiceFactoryDep) => { + const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRotation + ); + + return { + custom: [], + providers: rotationTemplates + }; + }; + + const createRotation = async ({ + projectId, + actorId, + actor, + inputs, + outputs, + interval, + provider, + secretPath, + environment + }: TCreateSecretRotationDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRotation + ); + const env = await projectEnvDal.findOne({ slug: environment }); + if (!env) throw new BadRequestError({ message: "Environment not found" }); + + const selectedTemplate = rotationTemplates.find(({ name }) => name === provider); + if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" }); + const formattedInputs: Record = {}; + Object.entries(inputs).forEach(([key, value]) => { + const { type } = selectedTemplate.template.inputs.properties[key]; + if (type === "string") { + formattedInputs[key] = value; + return; + } + if (type === "integer") { + formattedInputs[key] = parseInt(value as string, 10); + return; + } + formattedInputs[key] = JSON.parse(value as string); + }); + // ensure input one follows the correct schema + const valid = ajv.validate(selectedTemplate.template.inputs, formattedInputs); + if (!valid) { + throw new BadRequestError({ message: ajv.errors?.[0].message }); + } + + const unencryptedData: Partial = { + inputs: formattedInputs, + creds: [] + }; + const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData)); + const secretRotation = secretRotationDal.transaction(async (tx) => { + const doc = await secretRotationDal.create( + { + provider, + secretPath, + interval, + envId: env.id, + encryptedDataTag: encData.tag, + encryptedDataIV: encData.iv, + encryptedData: encData.ciphertext, + algorithm: encData.algorithm, + keyEncoding: encData.encoding + }, + tx + ); + await secretRotationQueue.addToQueue(doc.id, doc.interval); + const outputSecretMapping = await secretRotationDal.secretOutputInsertMany( + Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })), + tx + ); + return { ...doc, outputs: outputSecretMapping, environment: env }; + }); + return secretRotation; + }; + + const getById = async ({ rotationId, actor, actorId }: TGetByIdDTO) => { + const [doc] = await secretRotationDal.find({ id: rotationId }); + if (!doc) throw new BadRequestError({ message: "Rotation not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + doc.projectId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRotation + ); + return doc; + }; + + const getByProjectId = async ({ actorId, projectId, actor }: TListByProjectIdDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRotation + ); + const doc = await secretRotationDal.find({ projectId }); + return doc; + }; + + const restartById = async ({ actor, actorId, rotationId }: TRestartDTO) => { + const doc = await secretRotationDal.findById(rotationId); + if (!doc) throw new BadRequestError({ message: "Rotation not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + doc.projectId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.SecretRotation + ); + await secretRotationQueue.removeFromQueue(doc.id); + await secretRotationQueue.addToQueue(doc.id, doc.interval); + return doc; + }; + + const deleteById = async ({ actor, actorId, rotationId }: TDeleteDTO) => { + const doc = await secretRotationDal.findById(rotationId); + if (!doc) throw new BadRequestError({ message: "Rotation not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + doc.projectId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SecretRotation + ); + const deletedDoc = await secretRotationDal.transaction(async (tx) => { + const strat = await secretRotationDal.deleteById(rotationId, tx); + await secretRotationQueue.removeFromQueue(strat.id); + return strat; + }); + return { ...doc, ...deletedDoc }; + }; + + return { + getProviderTemplates, + getById, + getByProjectId, + createRotation, + restartById, + deleteById + }; +}; diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-types.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-types.ts new file mode 100644 index 000000000..52d248765 --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-types.ts @@ -0,0 +1,24 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TCreateSecretRotationDTO = { + secretPath: string; + environment: string; + interval: number; + provider: string; + inputs: Record; + outputs: Record; +} & TProjectPermission; + +export type TListByProjectIdDTO = TProjectPermission; + +export type TDeleteDTO = { + rotationId: string; +} & Omit; + +export type TRestartDTO = { + rotationId: string; +} & Omit; + +export type TGetByIdDTO = { + rotationId: string; +} & Omit; diff --git a/backend-pg/src/ee/services/secret-rotation/templates/index.ts b/backend-pg/src/ee/services/secret-rotation/templates/index.ts new file mode 100644 index 000000000..3d9fb2298 --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/templates/index.ts @@ -0,0 +1,28 @@ +import { MYSQL_TEMPLATE } from "./mysql"; +import { POSTGRES_TEMPLATE } from "./postgres"; +import { SENDGRID_TEMPLATE } from "./sendgrid"; +import { TSecretRotationProviderTemplate } from "./types"; + +export const rotationTemplates: TSecretRotationProviderTemplate[] = [ + { + name: "sendgrid", + title: "Twilio Sendgrid", + image: "sendgrid.png", + description: "Rotate Twilio Sendgrid API keys", + template: SENDGRID_TEMPLATE + }, + { + name: "postgres", + title: "PostgreSQL", + image: "postgres.png", + description: "Rotate PostgreSQL/CockroachDB user credentials", + template: POSTGRES_TEMPLATE + }, + { + name: "mysql", + title: "MySQL", + image: "mysql.png", + description: "Rotate MySQL@7/MariaDB user credentials", + template: MYSQL_TEMPLATE + } +]; diff --git a/backend-pg/src/ee/services/secret-rotation/templates/mysql.ts b/backend-pg/src/ee/services/secret-rotation/templates/mysql.ts new file mode 100644 index 000000000..86a382d6e --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/templates/mysql.ts @@ -0,0 +1,41 @@ +import { TDbProviderClients, TProviderFunctionTypes } from "./types"; + +export const MYSQL_TEMPLATE = { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.MySql, + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "3306" }, + username1: { + type: "string", + default: "infisical-sql-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-sql-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + } +}; diff --git a/backend-pg/src/ee/services/secret-rotation/templates/postgres.ts b/backend-pg/src/ee/services/secret-rotation/templates/postgres.ts new file mode 100644 index 000000000..318ca650d --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/templates/postgres.ts @@ -0,0 +1,41 @@ +import { TDbProviderClients, TProviderFunctionTypes } from "./types"; + +export const POSTGRES_TEMPLATE = { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Pg as const, + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "5432" }, + username1: { + type: "string", + default: "infisical-pg-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-pg-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + } +}; diff --git a/backend-pg/src/ee/services/secret-rotation/templates/sendgrid.ts b/backend-pg/src/ee/services/secret-rotation/templates/sendgrid.ts new file mode 100644 index 000000000..bfdda7606 --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/templates/sendgrid.ts @@ -0,0 +1,62 @@ +/* eslint-disable no-template-curly-in-string */ +import { TAssignOp, TProviderFunctionTypes } from "./types"; + +export const SENDGRID_TEMPLATE = { + type: TProviderFunctionTypes.HTTP as const, + inputs: { + type: "object" as const, + properties: { + admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" }, + api_key_scopes: { + type: "array", + items: { type: "string" as const }, + desc: "Scopes for created tokens by rotation(Array)" + } + }, + required: ["admin_api_key", "api_key_scopes"], + additionalProperties: false + }, + outputs: { + api_key: { type: "string" } + }, + internal: { + api_key_id: { type: "string" } + }, + functions: { + set: { + url: "https://api.sendgrid.com/v3/api_keys", + method: "POST", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + body: { + name: "infisical-${random | 16}", + scopes: { ref: "inputs.api_key_scopes" } + }, + setter: { + "outputs.api_key": { + assign: TAssignOp.JmesPath as const, + path: "api_key" + }, + "internal.api_key_id": { + assign: TAssignOp.JmesPath as const, + path: "api_key_id" + } + } + }, + remove: { + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "DELETE" + }, + test: { + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "GET" + } + } +}; diff --git a/backend-pg/src/ee/services/secret-rotation/templates/types.ts b/backend-pg/src/ee/services/secret-rotation/templates/types.ts new file mode 100644 index 000000000..cb48a7782 --- /dev/null +++ b/backend-pg/src/ee/services/secret-rotation/templates/types.ts @@ -0,0 +1,72 @@ +export enum TProviderFunctionTypes { + HTTP = "http", + DB = "database" +} + +export enum TDbProviderClients { + // postgres, cockroack db, amazon red shift + Pg = "pg", + // mysql and maria db + MySql = "mysql" +} + +export enum TAssignOp { + Direct = "direct", + JmesPath = "jmesopath" +} + +export type TJmesPathAssignOp = { + assign: TAssignOp.JmesPath; + path: string; +}; + +export type TDirectAssignOp = { + assign: TAssignOp.Direct; + value: string; +}; + +export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp; + +export type THttpProviderFunction = { + url: string; + method: string; + header?: Record; + query?: Record; + body?: Record; + setter?: Record; + pre?: Record; +}; + +export type TSecretRotationProviderTemplate = { + name: string; + title: string; + image?: string; + description?: string; + template: THttpProviderTemplate | TDbProviderTemplate; +}; + +export type THttpProviderTemplate = { + type: TProviderFunctionTypes.HTTP; + inputs: { + type: "object"; + properties: Record; + required?: string[]; + }; + outputs: Record; + functions: { + set: THttpProviderFunction; + remove?: THttpProviderFunction; + test: THttpProviderFunction; + }; +}; + +export type TDbProviderTemplate = { + type: TProviderFunctionTypes.DB; + client: TDbProviderClients; + inputs: { + type: "object"; + properties: Record; + required?: string[]; + }; + outputs: Record; +}; diff --git a/backend-pg/src/lib/config/env.ts b/backend-pg/src/lib/config/env.ts index e75f05d2a..1fef7e199 100644 --- a/backend-pg/src/lib/config/env.ts +++ b/backend-pg/src/lib/config/env.ts @@ -13,6 +13,7 @@ const zodStrBool = z const envSchema = z .object({ PORT: z.coerce.number().default(4000), + REDIS_URL: zpStr(z.string()), HOST: zpStr(z.string().default("localhost")), DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")), NODE_ENV: z.enum(["development", "test", "production"]).default("development"), diff --git a/backend-pg/src/lib/crypto/encryption.ts b/backend-pg/src/lib/crypto/encryption.ts index add6d2ccc..18b6b7312 100644 --- a/backend-pg/src/lib/crypto/encryption.ts +++ b/backend-pg/src/lib/crypto/encryption.ts @@ -6,6 +6,8 @@ import naclUtils from "tweetnacl-util"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; +import { getConfig } from "../config/env"; + export type TDecryptSymmetricInput = { ciphertext: string; iv: string; @@ -191,3 +193,52 @@ export const createSecretBlindIndex = (rootEncryptionKey?: string, encryptionKey } throw new Error("Failed to generate blind index due to encryption key missing"); }; + +export const infisicalSymmetricEncypt = (data: string) => { + const appCfg = getConfig(); + const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; + const encryptionKey = appCfg.ENCRYPTION_KEY; + if (rootEncryptionKey) { + const { iv, tag, ciphertext } = encryptSymmetric(data, rootEncryptionKey); + return { + iv, + tag, + ciphertext, + algorithm: SecretEncryptionAlgo.AES_256_GCM, + encoding: SecretKeyEncoding.BASE64 + }; + } + if (encryptionKey) { + const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(data, encryptionKey); + return { + iv, + tag, + ciphertext, + algorithm: SecretEncryptionAlgo.AES_256_GCM, + encoding: SecretKeyEncoding.UTF8 + }; + } + throw new Error("Missing both encryption keys"); +}; + +export const infisicalSymmetricDecrypt = ({ + keyEncoding, + ciphertext, + tag, + iv +}: Omit & { + keyEncoding: SecretKeyEncoding; +}) => { + const appCfg = getConfig(); + const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; + const encryptionKey = appCfg.ENCRYPTION_KEY; + if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) { + const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext }); + return data as T; + } + if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) { + const data = decryptSymmetric128BitHexKeyUTF8({ key: encryptionKey, iv, tag, ciphertext }); + return data as T; + } + throw new Error("Missing both encryption keys"); +}; diff --git a/backend-pg/src/lib/dates/index.ts b/backend-pg/src/lib/dates/index.ts new file mode 100644 index 000000000..b41a02a06 --- /dev/null +++ b/backend-pg/src/lib/dates/index.ts @@ -0,0 +1 @@ +export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000; diff --git a/backend-pg/src/lib/errors/index.ts b/backend-pg/src/lib/errors/index.ts index 0865c1267..5699b7627 100644 --- a/backend-pg/src/lib/errors/index.ts +++ b/backend-pg/src/lib/errors/index.ts @@ -17,7 +17,7 @@ export class UnauthorizedError extends Error { error: unknown; constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) { - super(message ?? "You are not allowed to access this resourve"); + super(message ?? "You are not allowed to access this resource"); this.name = name || "UnauthorizedError"; this.error = error; } @@ -29,7 +29,7 @@ export class ForbiddenRequestError extends Error { error: unknown; constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) { - super(message ?? "You are not allowed to access this resourve"); + super(message ?? "You are not allowed to access this resource"); this.name = name || "ForbideenError"; this.error = error; } diff --git a/backend-pg/src/lib/knex/join.ts b/backend-pg/src/lib/knex/join.ts index 73999c2ff..884934f50 100644 --- a/backend-pg/src/lib/knex/join.ts +++ b/backend-pg/src/lib/knex/join.ts @@ -48,7 +48,7 @@ export type TChildMapper> = { - [K in T["label"]]: ReturnType["mapper"]>[]; + [K in T["label"]]: Exclude["mapper"]>, null | undefined>[]; }; export const sqlNestRelationships = < diff --git a/backend-pg/src/lib/knex/select.ts b/backend-pg/src/lib/knex/select.ts index b7cc08e6d..c183907b6 100644 --- a/backend-pg/src/lib/knex/select.ts +++ b/backend-pg/src/lib/knex/select.ts @@ -2,3 +2,13 @@ import { Tables } from "knex/types/tables"; export const selectAllTableCols = (tableName: Tname) => `${tableName}.*` as keyof Tables[Tname]["base"]; + +export const stripUndefinedInWhere = (val: T) => { + const copy = val; + Object.entries(copy).forEach(([key, value]) => { + if (typeof value === "undefined") { + delete copy[key as keyof T]; + } + }); + return copy; +}; diff --git a/backend-pg/src/main.ts b/backend-pg/src/main.ts index 6cf1ec6e5..56c63fbfe 100644 --- a/backend-pg/src/main.ts +++ b/backend-pg/src/main.ts @@ -3,6 +3,7 @@ import dotenv from "dotenv"; import { initDbConnection } from "./db"; import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { initLogger } from "./lib/logger"; +import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; import { smtpServiceFactory } from "./services/smtp/smtp-service"; @@ -12,8 +13,9 @@ const run = async () => { const appCfg = initEnvConfig(logger); const db = initDbConnection(appCfg.DB_CONNECTION_URI); const smtp = smtpServiceFactory(formatSmtpConfig()); + const queue = queueServiceFactory(appCfg.REDIS_URL); - const server = await main({ db, smtp, logger }); + const server = await main({ db, smtp, logger, queue }); process.on("SIGINT", async () => { await server.close(); await db.destroy(); diff --git a/backend-pg/src/queue/index.ts b/backend-pg/src/queue/index.ts new file mode 100644 index 000000000..638e4715a --- /dev/null +++ b/backend-pg/src/queue/index.ts @@ -0,0 +1,7 @@ +export { + QueueJobs, + QueueName, + queueServiceFactory, + TQueueJobTypes, + TQueueServiceFactory +} from "./queue-service"; diff --git a/backend-pg/src/queue/queue-service.ts b/backend-pg/src/queue/queue-service.ts new file mode 100644 index 000000000..0833555b5 --- /dev/null +++ b/backend-pg/src/queue/queue-service.ts @@ -0,0 +1,87 @@ +import { Job, JobsOptions, Queue, Worker, WorkerListener } from "bullmq"; +import Redis from "ioredis"; + +export enum QueueName { + SecretRotation = "secret-rotation" +} + +export enum QueueJobs { + SecretRotation = "secret-rotation-job" +} + +export type TQueueJobTypes = { + [QueueName.SecretRotation]: { + payload: { rotationId: string }; + name: QueueJobs.SecretRotation; + }; +}; + +export type TQueueServiceFactory = ReturnType; +export const queueServiceFactory = (redisUrl: string) => { + const connection = new Redis(redisUrl); + const queueContainer: Record< + QueueName, + Queue + > = {} as any; + const workerContainer: Record< + QueueName, + Worker + > = {} as any; + + const start = ( + name: T, + jobFn: ( + job: Job + ) => Promise + ) => { + if (queueContainer[name]) { + throw new Error(`${name} queue is already initialized`); + } + + queueContainer[name] = new Queue( + name as string, + { connection } + ); + workerContainer[name] = new Worker< + TQueueJobTypes[T]["payload"], + void, + TQueueJobTypes[T]["name"] + >(name, jobFn); + }; + + const listen = async < + T extends QueueName, + U extends keyof WorkerListener + >( + name: T, + event: U, + listener: WorkerListener[U] + ) => { + const worker = workerContainer[name]; + worker.on(event, listener); + }; + + const queue = async ( + name: T, + job: TQueueJobTypes[T]["name"], + data: TQueueJobTypes[T]["payload"], + opts: JobsOptions & { jobId: string } + ) => { + const q = queueContainer[name]; + await q.add(job, data, opts); + }; + + const stopRepeatableJob = async (name: T, jobId: string) => { + const q = queueContainer[name]; + const job = await q.getJob(jobId); + if (!job) return true; + if (!job.repeatJobKey) return true; + return q.removeRepeatableByKey(job.repeatJobKey); + }; + + const shutdown = async () => { + await Promise.all(Object.values(workerContainer).map((worker) => worker.close())); + }; + + return { start, listen, queue, shutdown, stopRepeatableJob }; +}; diff --git a/backend-pg/src/server/app.ts b/backend-pg/src/server/app.ts index 8ab5fbf3e..7b1842a63 100644 --- a/backend-pg/src/server/app.ts +++ b/backend-pg/src/server/app.ts @@ -9,6 +9,7 @@ import fasitfy from "fastify"; import { Knex } from "knex"; import { Logger } from "pino"; +import { TQueueServiceFactory } from "@app/queue"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { getConfig } from "@lib/config/env"; @@ -23,10 +24,11 @@ type TMain = { db: Knex; smtp: TSmtpService; logger?: Logger; + queue: TQueueServiceFactory; }; // Run the server! -export const main = async ({ db, smtp, logger }: TMain) => { +export const main = async ({ db, smtp, logger, queue }: TMain) => { const appCfg = getConfig(); const server = fasitfy({ logger, @@ -54,12 +56,13 @@ export const main = async ({ db, smtp, logger }: TMain) => { await server.register(ratelimiter, globalRateLimiterCfg); await server.register(helmet, { contentSecurityPolicy: false }); - await server.register(registerRoutes, { prefix: "/api", smtp, db }); + await server.register(registerRoutes, { prefix: "/api", smtp, queue, db }); await server.ready(); server.swagger(); return server; } catch (err) { server.log.error(err); + await queue.shutdown(); process.exit(1); } }; diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts index d3c603e65..7999c3fe4 100644 --- a/backend-pg/src/server/routes/index.ts +++ b/backend-pg/src/server/routes/index.ts @@ -4,7 +4,18 @@ import { z } from "zod"; import { registerV1EERoutes } from "@app/ee/routes/v1"; import { permissionDalFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { sapApproverDalFactory } from "@app/ee/services/secret-approval-policy/sap-approver-dal"; +import { secretApprovalPolicyDalFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal"; +import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; +import { sarReviewerDalFactory } from "@app/ee/services/secret-approval-request/sar-reviewer-dal"; +import { sarSecretDalFactory } from "@app/ee/services/secret-approval-request/sar-secret-dal"; +import { secretApprovalRequestDalFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; +import { secretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service"; +import { secretRotationDalFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; +import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue"; +import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; import { getConfig } from "@app/lib/config/env"; +import { TQueueServiceFactory } from "@app/queue"; import { apiKeyDalFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { authDalFactory } from "@app/services/auth/auth-dal"; @@ -69,13 +80,14 @@ import { injectPermission } from "../plugins/auth/inject-permission"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; -import { secretApprovalPolicyDalFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal"; -import { sapApproverDalFactory } from "@app/ee/services/secret-approval-policy/sap-approver-dal"; -import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; export const registerRoutes = async ( server: FastifyZodProvider, - { db, smtp: smtpService }: { db: Knex; smtp: TSmtpService } + { + db, + smtp: smtpService, + queue: queueService + }: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory } ) => { // db layers const userDal = userDalFactory(db); @@ -118,8 +130,12 @@ export const registerRoutes = async ( const permissionDal = permissionDalFactory(db); const sapApproverDal = sapApproverDalFactory(db); const secretApprovalPolicyDal = secretApprovalPolicyDalFactory(db); + const secretApprovalRequestDal = secretApprovalRequestDalFactory(db); + const sarReviewerDal = sarReviewerDalFactory(db); + const sarSecretDal = sarSecretDalFactory(db); + + const secretRotationDal = secretRotationDalFactory(db); - // ee services const permissionService = permissionServiceFactory({ permissionDal, orgRoleDal, projectRoleDal }); const sapService = secretApprovalPolicyServiceFactory({ projectMembershipDal, @@ -128,8 +144,17 @@ export const registerRoutes = async ( permissionService, secretApprovalPolicyDal }); + const sarService = secretApprovalRequestServiceFactory({ + permissionService, + folderDal, + secretDal, + sarSecretDal, + sarReviewerDal, + secretVersionDal, + secretBlindIndexDal, + secretApprovalRequestDal + }); - // service layers const tokenService = tokenServiceFactory({ tokenDal: authTokenDal }); const userService = userServiceFactory({ userDal }); const loginService = authLoginServiceFactory({ userDal, smtpService, tokenService }); @@ -211,6 +236,21 @@ export const registerRoutes = async ( secretImportDal }); const projectBotService = projectBotServiceFactory({ permissionService, projectBotDal }); + + const secretRotationQueue = secretRotationQueueFactory({ + secretRotationDal, + queue: queueService, + secretDal, + secretVersionDal, + projectBotService + }); + const secretRotationService = secretRotationServiceFactory({ + permissionService, + projectEnvDal, + secretRotationDal, + secretRotationQueue + }); + const integrationService = integrationServiceFactory({ permissionService, folderDal, @@ -287,7 +327,9 @@ export const registerRoutes = async ( identityAccessToken: identityAccessTokenService, identityProject: identityProjectService, identityUa: identityUaService, - secretApprovalPolicy: sapService + secretApprovalPolicy: sapService, + secretApprovalRequest: sarService, + secretRotation: secretRotationService }); server.decorate("store", { diff --git a/backend-pg/src/server/routes/v3/secret-router.ts b/backend-pg/src/server/routes/v3/secret-router.ts index 033617238..38352f343 100644 --- a/backend-pg/src/server/routes/v3/secret-router.ts +++ b/backend-pg/src/server/routes/v3/secret-router.ts @@ -1,8 +1,9 @@ import { z } from "zod"; -import { SecretsSchema,SecretType } from "@app/db/schemas"; +import { SecretApprovalRequestsSchema, SecretsSchema, SecretType } from "@app/db/schemas"; +import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ @@ -102,32 +103,92 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: z.string().trim() }), response: { - 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }) - }) + 200: z.union([ + z.object({ + secret: SecretsSchema.omit({ secretBlindIndex: true }) + }), + z + .object({ approval: SecretApprovalRequestsSchema }) + .describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { + const { + workspaceId: projectId, + secretPath, + environment, + metadata, + type, + secretKeyIV, + secretKeyTag, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretKeyCiphertext, + secretValueCiphertext, + secretCommentCiphertext, + skipMultilineEncoding + } = req.body; + if (req.body.type !== SecretType.Personal && req.permission.type === ActorType.USER) { + const policy = await server.services.secretApprovalPolicy.getSapOfFolder({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId + }); + if (policy) { + const approval = + await server.services.secretApprovalRequest.generateSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId, + policy, + data: { + [CommitType.Create]: [ + { + secretName: req.params.secretName, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretKeyTag, + secretKeyCiphertext, + secretKeyIV + } + ] + } + }); + return { approval }; + } + } const secret = await server.services.secret.createSecret({ actorId: req.permission.id, actor: req.permission.type, - path: req.body.secretPath, - type: req.body.type, + path: secretPath, + type, environment: req.body.environment, secretName: req.params.secretName, - projectId: req.body.workspaceId, - secretKeyIV: req.body.secretKeyIV, - secretKeyTag: req.body.secretKeyTag, - secretKeyCiphertext: req.body.secretKeyCiphertext, - secretValueIV: req.body.secretValueIV, - secretValueTag: req.body.secretValueTag, - secretValueCiphertext: req.body.secretValueCiphertext, - secretCommentIV: req.body.secretCommentIV, - secretCommentTag: req.body.secretCommentTag, - secretCommentCiphertext: req.body.secretCommentCiphertext, - skipMultilineEncoding: req.body.skipMultilineEncoding, - metadata: req.body.metadata + projectId, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueIV, + secretValueTag, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + metadata }); return { secret }; @@ -165,35 +226,103 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: z.record(z.string()).optional() }), response: { - 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }) - }) + 200: z.union([ + z.object({ + secret: SecretsSchema.omit({ secretBlindIndex: true }) + }), + z + .object({ approval: SecretApprovalRequestsSchema }) + .describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { + const { + secretValueCiphertext, + secretValueTag, + secretValueIV, + type, + environment, + secretPath, + workspaceId: projectId, + tags, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + secretName: newSecretName, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + skipMultilineEncoding, + secretReminderRepeatDays, + secretReminderNote, + metadata + } = req.body; + + if (req.body.type !== SecretType.Personal && req.permission.type === ActorType.USER) { + const policy = await server.services.secretApprovalPolicy.getSapOfFolder({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId + }); + if (policy) { + const approval = + await server.services.secretApprovalRequest.generateSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId, + policy, + data: { + [CommitType.Update]: [ + { + secretName: req.params.secretName, + newSecretName, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + secretKeyTag, + secretKeyCiphertext, + secretKeyIV + } + ] + } + }); + return { approval }; + } + } + const secret = await server.services.secret.updateSecret({ actorId: req.permission.id, actor: req.permission.type, - path: req.body.secretPath, - type: req.body.type, - environment: req.body.environment, + path: secretPath, + type, + environment, secretName: req.params.secretName, - projectId: req.body.workspaceId, - secretKeyIV: req.body.secretKeyIV, - secretKeyTag: req.body.secretKeyTag, - secretKeyCiphertext: req.body.secretKeyCiphertext, - secretValueIV: req.body.secretValueIV, - secretValueTag: req.body.secretValueTag, - secretValueCiphertext: req.body.secretValueCiphertext, - secretCommentIV: req.body.secretCommentIV, - secretCommentTag: req.body.secretCommentTag, - secretCommentCiphertext: req.body.secretCommentCiphertext, - skipMultilineEncoding: req.body.skipMultilineEncoding, - metadata: req.body.metadata, - secretReminderRepeatDays: req.body.secretReminderRepeatDays, - secretReminderNote: req.body.secretReminderNote, - newSecretName: req.body.secretName + projectId, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + secretValueIV, + tags, + secretValueTag, + secretValueCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentCiphertext, + skipMultilineEncoding, + metadata, + secretReminderRepeatDays, + secretReminderNote, + newSecretName }); return { secret }; @@ -215,22 +344,57 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: z.string().trim() }), response: { - 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }) - }) + 200: z.union([ + z.object({ + secret: SecretsSchema.omit({ secretBlindIndex: true }) + }), + z + .object({ approval: SecretApprovalRequestsSchema }) + .describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { + const { secretPath, type, workspaceId: projectId, secretId, environment } = req.body; + if (req.body.type !== SecretType.Personal && req.permission.type === ActorType.USER) { + const policy = await server.services.secretApprovalPolicy.getSapOfFolder({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId + }); + if (policy) { + const approval = + await server.services.secretApprovalRequest.generateSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId, + policy, + data: { + [CommitType.Delete]: [ + { + secretName: req.params.secretName + } + ] + } + }); + return { approval }; + } + } + const secret = await server.services.secret.deleteSecret({ actorId: req.permission.id, actor: req.permission.type, - path: req.body.secretPath, - type: req.body.type, - environment: req.body.environment, + path: secretPath, + type, + environment, secretName: req.params.secretName, - projectId: req.body.workspaceId, - secretId: req.body.secretId + projectId, + secretId }); return { secret }; @@ -265,20 +429,51 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .min(1) }), response: { - 200: z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() - }) + 200: z.union([ + z.object({ + secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + }), + z + .object({ approval: SecretApprovalRequestsSchema }) + .describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { + const { environment, workspaceId: projectId, secretPath, secrets: inputSecrets } = req.body; + if (req.permission.type === ActorType.USER) { + const policy = await server.services.secretApprovalPolicy.getSapOfFolder({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId + }); + if (policy) { + const approval = + await server.services.secretApprovalRequest.generateSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId, + policy, + data: { + [CommitType.Create]: inputSecrets.filter(({ type }) => type === "shared") + } + }); + return { approval }; + } + } + const secrets = await server.services.secret.createManySecret({ actorId: req.permission.id, actor: req.permission.type, - path: req.body.secretPath, - environment: req.body.environment, - projectId: req.body.workspaceId, - secrets: req.body.secrets + path: secretPath, + environment, + projectId, + secrets: inputSecrets }); return { secrets }; @@ -313,20 +508,50 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .min(1) }), response: { - 200: z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() - }) + 200: z.union([ + z.object({ + secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + }), + z + .object({ approval: SecretApprovalRequestsSchema }) + .describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { + const { environment, workspaceId: projectId, secretPath, secrets: inputSecrets } = req.body; + if (req.permission.type === ActorType.USER) { + const policy = await server.services.secretApprovalPolicy.getSapOfFolder({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId + }); + if (policy) { + const approval = + await server.services.secretApprovalRequest.generateSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId, + policy, + data: { + [CommitType.Update]: inputSecrets.filter(({ type }) => type === "shared") + } + }); + return { approval }; + } + } const secrets = await server.services.secret.updateManySecret({ actorId: req.permission.id, actor: req.permission.type, - path: req.body.secretPath, - environment: req.body.environment, - projectId: req.body.workspaceId, - secrets: req.body.secrets + path: secretPath, + environment, + projectId, + secrets: inputSecrets }); return { secrets }; @@ -350,20 +575,50 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .min(1) }), response: { - 200: z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() - }) + 200: z.union([ + z.object({ + secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + }), + z + .object({ approval: SecretApprovalRequestsSchema }) + .describe("When secret protection policy is enabled") + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { + const { environment, workspaceId: projectId, secretPath, secrets: inputSecrets } = req.body; + if (req.permission.type === ActorType.USER) { + const policy = await server.services.secretApprovalPolicy.getSapOfFolder({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId + }); + if (policy) { + const approval = + await server.services.secretApprovalRequest.generateSecretApprovalRequest({ + actorId: req.permission.id, + actor: req.permission.type, + secretPath, + environment, + projectId, + policy, + data: { + [CommitType.Delete]: inputSecrets.filter(({ type }) => type === "shared") + } + }); + return { approval }; + } + } const secrets = await server.services.secret.deleteManySecret({ actorId: req.permission.id, actor: req.permission.type, path: req.body.secretPath, - environment: req.body.environment, - projectId: req.body.workspaceId, - secrets: req.body.secrets + environment, + projectId, + secrets: inputSecrets }); return { secrets }; diff --git a/backend-pg/src/services/secret/secret-dal.ts b/backend-pg/src/services/secret/secret-dal.ts index 12e19357a..36df01d88 100644 --- a/backend-pg/src/services/secret/secret-dal.ts +++ b/backend-pg/src/services/secret/secret-dal.ts @@ -29,11 +29,13 @@ export const secretDalFactory = (db: TDbClient) => { // the idea is to use postgres specific function // insert with id this will cause a conflict then merge the data - const bulkUpdate = async (data: Array, tx?: Knex) => { + const bulkUpdate = async ( + data: Array, + tx?: Knex + ) => { try { const secs = await (tx || db)(TableName.Secret) .insert(data as TSecretsInsert[]) - .increment("version", 1) .onConflict("id") .merge() .returning("*"); diff --git a/backend-pg/src/services/secret/secret-version-dal.ts b/backend-pg/src/services/secret/secret-version-dal.ts index 28174c339..dcf3ee4a4 100644 --- a/backend-pg/src/services/secret/secret-version-dal.ts +++ b/backend-pg/src/services/secret/secret-version-dal.ts @@ -1,8 +1,9 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TSecretVersions, TableName } from "@app/db/schemas"; +import { TableName,TSecretVersions } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; -import { Knex } from "knex"; export type TSecretVersionDalFactory = ReturnType; diff --git a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx index 94358ed79..4eb84c352 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx @@ -14,7 +14,7 @@ export const useUpdateSecretApprovalReviewStatus = () => { return useMutation<{}, {}, TUpdateSecretApprovalReviewStatusDTO>({ mutationFn: async ({ id, status }) => { - const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/review`, { + const { data } = await apiRequest.post(`/api/ee/v1/secret-approval-requests/${id}/review`, { status }); return data; @@ -30,7 +30,7 @@ export const useUpdateSecretApprovalRequestStatus = () => { return useMutation<{}, {}, TUpdateSecretApprovalRequestStatusDTO>({ mutationFn: async ({ id, status }) => { - const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/status`, { + const { data } = await apiRequest.post(`/api/ee/v1/secret-approval-requests/${id}/status`, { status }); return data; @@ -47,7 +47,7 @@ export const usePerformSecretApprovalRequestMerge = () => { return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({ mutationFn: async ({ id }) => { - const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`); + const { data } = await apiRequest.post(`/api/ee/v1/secret-approval-requests/${id}/merge`); return data; }, onSuccess: (_, { id, workspaceId }) => { diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx index 11325fb50..94f4eda57 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -15,12 +15,12 @@ import { UserWsKeyPair } from "../keys/types"; import { decryptSecrets } from "../secrets/queries"; import { DecryptedSecret } from "../secrets/types"; import { + CommitType, TGetSecretApprovalRequestCount, TGetSecretApprovalRequestDetails, TGetSecretApprovalRequestList, TSecretApprovalRequest, TSecretApprovalRequestCount, - TSecretApprovalSecChange, TSecretApprovalSecChangeData } from "./types"; @@ -96,7 +96,7 @@ const fetchSecretApprovalRequestList = async ({ offset }: TGetSecretApprovalRequestList) => { const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>( - "/api/v1/secret-approval-requests", + "/api/ee/v1/secret-approval-requests", { params: { workspaceId, @@ -158,7 +158,7 @@ const fetchSecretApprovalRequestDetails = async ({ id }: Omit) => { const { data } = await apiRequest.get<{ approval: TSecretApprovalRequest }>( - `/api/v1/secret-approval-requests/${id}` + `/api/ee/v1/secret-approval-requests/${id}` ); return data.approval; @@ -173,7 +173,7 @@ export const useGetSecretApprovalRequestDetails = ({ UseQueryOptions< TSecretApprovalRequest, unknown, - TSecretApprovalRequest, + TSecretApprovalRequest, ReturnType >, "queryKey" | "queryFn" @@ -184,11 +184,12 @@ export const useGetSecretApprovalRequestDetails = ({ queryFn: () => fetchSecretApprovalRequestDetails({ id }), select: (data) => ({ ...data, - commits: data.commits.map(({ secretVersion, op, newVersion, secret }) => ({ + commits: data.commits.map(({ secretVersion, op, secret, ...newVersion }) => ({ op, secret, secretVersion: secretVersion ? decryptSecrets([secretVersion], decryptKey)[0] : undefined, - newVersion: newVersion ? decryptSecretApprovalSecret(newVersion, decryptKey) : undefined + newVersion: + op !== CommitType.DELETE ? decryptSecretApprovalSecret(newVersion, decryptKey) : undefined })) }), enabled: Boolean(id && decryptKey) && (options?.enabled ?? true) @@ -196,7 +197,7 @@ export const useGetSecretApprovalRequestDetails = ({ const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => { const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>( - "/api/v1/secret-approval-requests/count", + "/api/ee/v1/secret-approval-requests/count", { params: { workspaceId } } ); diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index 1364701a5..27b615649 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -42,10 +42,7 @@ export type TSecretApprovalSecChange = { tags?: string[]; }; -export type TSecretApprovalRequest< - T extends unknown = TSecretApprovalSecChangeData, - J extends unknown = EncryptedSecret -> = { +export type TSecretApprovalRequest = { id: string; slug: string; createdAt: string; @@ -63,14 +60,13 @@ export type TSecretApprovalRequest< policy: TSecretApprovalPolicy; statusChangeBy: string; conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>; - commits: { + commits: ({ // if there is no secret means it was creation secret?: { version: number }; secretVersion: J; // if there is no new version its for Delete - newVersion?: T; op: CommitType; - }[]; + } & TSecretApprovalSecChangeData)[]; }; export type TSecretApprovalRequestCount = { diff --git a/frontend/src/hooks/api/secretRotation/mutation.tsx b/frontend/src/hooks/api/secretRotation/mutation.tsx index cda4f8074..eb71e6e9d 100644 --- a/frontend/src/hooks/api/secretRotation/mutation.tsx +++ b/frontend/src/hooks/api/secretRotation/mutation.tsx @@ -14,7 +14,7 @@ export const useCreateSecretRotation = () => { return useMutation<{}, {}, TCreateSecretRotationDTO>({ mutationFn: async (dto) => { - const { data } = await apiRequest.post("/api/v1/secret-rotations", dto); + const { data } = await apiRequest.post("/api/ee/v1/secret-rotations", dto); return data; }, onSuccess: (_, { workspaceId }) => { @@ -28,7 +28,7 @@ export const useDeleteSecretRotation = () => { return useMutation<{}, {}, TDeleteSecretRotationDTO>({ mutationFn: async (dto) => { - const { data } = await apiRequest.delete(`/api/v1/secret-rotations/${dto.id}`); + const { data } = await apiRequest.delete(`/api/ee/v1/secret-rotations/${dto.id}`); return data; }, onSuccess: (_, { workspaceId }) => { @@ -42,7 +42,7 @@ export const useRestartSecretRotation = () => { return useMutation<{}, {}, TRestartSecretRotationDTO>({ mutationFn: async (dto) => { - const { data } = await apiRequest.post("/api/v1/secret-rotations/restart", { id: dto.id }); + const { data } = await apiRequest.post("/api/ee/v1/secret-rotations/restart", { id: dto.id }); return data; }, onSuccess: (_, { workspaceId }) => { diff --git a/frontend/src/hooks/api/secretRotation/queries.tsx b/frontend/src/hooks/api/secretRotation/queries.tsx index a3c8e270e..c3a107137 100644 --- a/frontend/src/hooks/api/secretRotation/queries.tsx +++ b/frontend/src/hooks/api/secretRotation/queries.tsx @@ -25,7 +25,7 @@ export const secretRotationKeys = { const fetchSecretRotationProviders = async ({ workspaceId }: TGetSecretRotationProviders) => { const { data } = await apiRequest.get( - `/api/v1/secret-rotation-providers/${workspaceId}` + `/api/ee/v1/secret-rotation-providers/${workspaceId}` ); return data; }; @@ -55,7 +55,7 @@ const fetchSecretRotations = async ({ workspaceId }: Omit) => { const { data } = await apiRequest.get<{ secretRotations: TSecretRotation[] }>( - "/api/v1/secret-rotations", + "/api/ee/v1/secret-rotations", { params: { workspaceId } } ); return data.secretRotations; diff --git a/frontend/src/hooks/api/secretRotation/types.ts b/frontend/src/hooks/api/secretRotation/types.ts index 90458f830..b8963a95e 100644 --- a/frontend/src/hooks/api/secretRotation/types.ts +++ b/frontend/src/hooks/api/secretRotation/types.ts @@ -1,5 +1,6 @@ import { UserWsKeyPair } from "../keys/types"; import { EncryptedSecret } from "../secrets/types"; +import { WorkspaceEnv } from "../workspace/types"; export enum TProviderFunctionTypes { HTTP = "http", @@ -31,7 +32,6 @@ export type TDirectAssignOp = { export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp; export type THttpProviderFunction = { - type: TProviderFunctionTypes.HTTP; url: string; method: string; header?: Record; @@ -41,42 +41,47 @@ export type THttpProviderFunction = { pre?: Record; }; -export type TDbProviderFunction = { - type: TProviderFunctionTypes.DB; - client: TDbProviderClients; - username: string; - password: string; - host: string; - database: string; - port: string; - query: string; - setter?: Record; - pre?: Record; +export type TSecretRotationProviderTemplate = { + name: string; + title: string; + image?: string; + description?: string; + template: THttpProviderTemplate | TDbProviderTemplate; }; -export type TProviderFunction = THttpProviderFunction | TDbProviderFunction; - -export type TProviderTemplate = { +export type THttpProviderTemplate = { + type: TProviderFunctionTypes.HTTP; inputs: { - properties: Record; type: "object"; + properties: Record; required: string[]; }; outputs: Record; functions: { - set: TProviderFunction; - remove?: TProviderFunction; - test: TProviderFunction; + set: THttpProviderFunction; + remove?: THttpProviderFunction; + test: THttpProviderFunction; }; }; +export type TDbProviderTemplate = { + type: TProviderFunctionTypes.DB; + inputs: { + type: "object"; + properties: Record; + required: string[]; + }; + outputs: Record; +}; + export type TSecretRotation = { id: string; interval: number; provider: string; customProvider: string; workspace: string; - environment: string; + envId: string; + environment: WorkspaceEnv; secretPath: string; outputs: Array<{ key: string; @@ -89,17 +94,9 @@ export type TSecretRotation = { keyEncoding: string; }; -export type TSecretRotationProvider = { - name: string; - image: string; - title: string; - description: string; - template: TProviderTemplate; -}; - export type TSecretRotationProviderList = { - custom: TSecretRotationProvider[]; - providers: TSecretRotationProvider[]; + custom: TSecretRotationProviderTemplate[]; + providers: TSecretRotationProviderTemplate[]; }; export type TGetSecretRotationProviders = { diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 7d514584a..04d215898 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -15,9 +15,8 @@ export type { TSecretFolder } from "./secretFolders/types"; export type { TImportedSecrets, TSecretImport } from "./secretImports/types"; export type { TGetSecretRotationProviders, - TProviderTemplate, - TSecretRotationProvider, - TSecretRotationProviderList + TSecretRotationProviderList, + TSecretRotationProviderTemplate } from "./secretRotation/types"; export * from "./secrets/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index 86cb26073..6b8fb6f80 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -93,6 +93,7 @@ export const SecretApprovalRequestChanges = ({ id: approvalRequestId, decryptKey: decryptFileKey! }); + console.log(secretApprovalRequestDetails); const { mutateAsync: updateSecretApprovalRequestStatus, diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx index 19c0a5040..8cdf9e0c4 100644 --- a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx @@ -53,7 +53,7 @@ import { useRestartSecretRotation, useUpdateBotActiveStatus } from "@app/hooks/api"; -import { TSecretRotationProvider } from "@app/hooks/api/types"; +import { TSecretRotationProviderTemplate } from "@app/hooks/api/types"; import { CreateRotationForm } from "./components/CreateRotationForm"; import { generateBotKey } from "./SecretRotationPage.utils"; @@ -143,7 +143,7 @@ export const SecretRotationPage = withProjectPermission( }; const handleUserAcceptBotCondition = async () => { - const provider = popUp.activeBot?.data as TSecretRotationProvider; + const provider = popUp.activeBot?.data as TSecretRotationProviderTemplate; try { if (bot?.id) { const botKey = generateBotKey(bot.publicKey, userWsKey!); @@ -165,7 +165,7 @@ export const SecretRotationPage = withProjectPermission( } }; - const handleCreateRotation = async (provider: TSecretRotationProvider) => { + const handleCreateRotation = async (provider: TSecretRotationProviderTemplate) => { if (subscription && !subscription?.secretRotation) { handlePopUpOpen("upgradePlan"); return; @@ -256,7 +256,7 @@ export const SecretRotationPage = withProjectPermission(
-
{environment}
+
{environment.slug}
{secretPath} @@ -389,7 +389,7 @@ export const SecretRotationPage = withProjectPermission( isOpen={popUp.createRotation.isOpen} workspaceId={workspaceId} onToggle={(isOpen) => handlePopUpToggle("createRotation", isOpen)} - provider={(popUp.createRotation.data as TSecretRotationProvider) || {}} + provider={(popUp.createRotation.data as TSecretRotationProviderTemplate) || {}} /> void; customProvider?: string; workspaceId: string; - provider: TSecretRotationProvider; + provider: TSecretRotationProviderTemplate; }; export const CreateRotationForm = ({