From 16f0ac6d431da1f43c3b769f873ff79ae211bafe Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 9 Jan 2024 16:18:25 +0530 Subject: [PATCH] feat(infisical-pg): completed audit log moved ee routes to normal routing pattern --- backend-pg/folder.ts | 89 --- backend-pg/package-lock.json | 241 ++++++- backend-pg/package.json | 1 + backend-pg/src/@types/fastify.d.ts | 7 + backend-pg/src/@types/knex.d.ts | 16 + .../db/migrations/20231218103423_secret.ts | 2 +- .../20231220052508_secret-version.ts | 2 +- .../migrations/20240107153439_saml-config.ts | 33 + .../db/migrations/20240107163155_org-bot.ts | 35 + .../db/migrations/20240108134148_audit-log.ts | 29 + backend-pg/src/db/schemas/audit-logs.ts | 28 + backend-pg/src/db/schemas/index.ts | 3 + backend-pg/src/db/schemas/models.ts | 3 + backend-pg/src/db/schemas/org-bots.ts | 31 + backend-pg/src/db/schemas/saml-configs.ts | 30 + backend-pg/src/db/schemas/secret-versions.ts | 2 +- backend-pg/src/db/schemas/secrets.ts | 2 +- backend-pg/src/ee/routes/v1/index.ts | 2 + backend-pg/src/ee/routes/v1/project-router.ts | 73 +- backend-pg/src/ee/routes/v1/saml-router.ts | 237 ++++++ .../v1/secret-approval-request-router.ts | 18 + .../ee/services/audit-log/audit-log-dal.ts | 61 ++ .../ee/services/audit-log/audit-log-queue.ts | 46 ++ .../services/audit-log/audit-log-service.ts | 73 ++ .../ee/services/audit-log/audit-log-types.ts | 674 ++++++++++++++++++ .../services/saml-config/saml-config-dal.ts | 10 + .../saml-config/saml-config-service.ts | 348 +++++++++ .../services/saml-config/saml-config-types.ts | 43 ++ .../secret-approval-request-dal.ts | 12 +- .../secret-approval-request-service.ts | 2 +- .../secret-rotation/secret-rotation-dal.ts | 4 +- .../services/secret-snapshot/snapshot-dal.ts | 15 +- backend-pg/src/folder.ts | 190 ----- backend-pg/src/lib/crypto/encryption.ts | 2 + backend-pg/src/lib/fn/object.ts | 20 + backend-pg/src/lib/knex/select.ts | 4 +- backend-pg/src/queue/queue-service.ts | 14 +- backend-pg/src/server/plugins/audit-log.ts | 55 ++ backend-pg/src/server/routes/index.ts | 38 +- .../src/server/routes/v1/identity-router.ts | 38 + .../src/server/routes/v1/identity-ua.ts | 118 ++- .../routes/v1/integration-auth-router.ts | 63 ++ .../server/routes/v1/integration-router.ts | 49 +- .../server/routes/v1/project-env-router.ts | 43 +- .../routes/v1/project-membership-router.ts | 83 ++- .../src/server/routes/v1/project-router.ts | 13 + .../server/routes/v1/secret-folder-router.ts | 43 +- .../server/routes/v1/secret-import-router.ts | 62 ++ .../src/server/routes/v1/webhook-router.ts | 53 +- .../src/server/routes/v2/project-router.ts | 12 + .../server/routes/v2/service-token-router.ts | 26 + .../src/server/routes/v3/secret-router.ts | 195 +++++ backend-pg/src/services/auth/auth-type.ts | 3 +- .../identity-ua/identity-ua-service.ts | 15 +- .../src/services/identity/identity-service.ts | 5 +- .../src/services/identity/identity-types.ts | 7 + .../integration-auth-service.ts | 2 +- .../integration-auth-types.ts | 2 +- .../integration/integration-service.ts | 4 +- backend-pg/src/services/org/org-bot-dal.ts | 10 + backend-pg/src/services/org/org-dal.ts | 13 +- backend-pg/src/services/org/org-service.ts | 44 +- .../project-env/project-env-service.ts | 7 +- .../project-membership-service.ts | 72 +- .../project-membership-types.ts | 8 + .../secret-folder/secret-folder-service.ts | 16 +- .../secret-import/secret-import-service.ts | 4 +- .../src/services/secret/secret-service.ts | 4 +- frontend/src/hooks/api/roles/mutation.tsx | 12 +- frontend/src/hooks/api/roles/queries.tsx | 8 +- .../src/hooks/api/secretApproval/mutation.tsx | 6 +- .../src/hooks/api/secretApproval/queries.tsx | 4 +- .../api/secretApprovalRequest/mutation.tsx | 6 +- .../api/secretApprovalRequest/queries.tsx | 6 +- .../src/hooks/api/secretRotation/mutation.tsx | 6 +- .../src/hooks/api/secretRotation/queries.tsx | 4 +- .../src/hooks/api/secretSnapshots/queries.tsx | 8 +- frontend/src/hooks/api/ssoConfig/queries.tsx | 54 +- 78 files changed, 3178 insertions(+), 445 deletions(-) delete mode 100644 backend-pg/folder.ts create mode 100644 backend-pg/src/db/migrations/20240107153439_saml-config.ts create mode 100644 backend-pg/src/db/migrations/20240107163155_org-bot.ts create mode 100644 backend-pg/src/db/migrations/20240108134148_audit-log.ts create mode 100644 backend-pg/src/db/schemas/audit-logs.ts create mode 100644 backend-pg/src/db/schemas/org-bots.ts create mode 100644 backend-pg/src/db/schemas/saml-configs.ts create mode 100644 backend-pg/src/ee/routes/v1/saml-router.ts create mode 100644 backend-pg/src/ee/services/audit-log/audit-log-dal.ts create mode 100644 backend-pg/src/ee/services/audit-log/audit-log-queue.ts create mode 100644 backend-pg/src/ee/services/audit-log/audit-log-service.ts create mode 100644 backend-pg/src/ee/services/audit-log/audit-log-types.ts create mode 100644 backend-pg/src/ee/services/saml-config/saml-config-dal.ts create mode 100644 backend-pg/src/ee/services/saml-config/saml-config-service.ts create mode 100644 backend-pg/src/ee/services/saml-config/saml-config-types.ts delete mode 100644 backend-pg/src/folder.ts create mode 100644 backend-pg/src/server/plugins/audit-log.ts create mode 100644 backend-pg/src/services/org/org-bot-dal.ts diff --git a/backend-pg/folder.ts b/backend-pg/folder.ts deleted file mode 100644 index 041fe8f74..000000000 --- a/backend-pg/folder.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 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 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 8068ad6b8..c9b6fa553 100644 --- a/backend-pg/package-lock.json +++ b/backend-pg/package-lock.json @@ -18,6 +18,7 @@ "@fastify/session": "^10.7.0", "@fastify/swagger": "^8.12.0", "@fastify/swagger-ui": "^1.10.1", + "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^20.0.2", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", @@ -996,6 +997,64 @@ "win32" ] }, + "node_modules/@node-saml/node-saml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz", + "integrity": "sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw==", + "dependencies": { + "@types/debug": "^4.1.7", + "@types/passport": "^1.0.11", + "@types/xml-crypto": "^1.4.2", + "@types/xml-encryption": "^1.2.1", + "@types/xml2js": "^0.4.11", + "@xmldom/xmldom": "^0.8.6", + "debug": "^4.3.4", + "xml-crypto": "^3.0.1", + "xml-encryption": "^3.0.2", + "xml2js": "^0.5.0", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@node-saml/node-saml/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/@node-saml/node-saml/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/@node-saml/passport-saml": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-4.0.4.tgz", + "integrity": "sha512-xFw3gw0yo+K1mzlkW15NeBF7cVpRHN/4vpjmBKzov5YFImCWh/G0LcTZ8krH3yk2/eRPc3Or8LRPudVJBjmYaw==", + "dependencies": { + "@node-saml/node-saml": "^4.0.4", + "@types/express": "^4.17.14", + "@types/passport": "^1.0.11", + "@types/passport-strategy": "^0.2.35", + "passport": "^0.6.0", + "passport-strategy": "^1.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1640,7 +1699,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -1650,16 +1708,22 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1671,7 +1735,6 @@ "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1682,8 +1745,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/jmespath": { "version": "0.15.2", @@ -1719,14 +1781,17 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { "version": "20.9.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", "integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1753,7 +1818,6 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", - "dev": true, "dependencies": { "@types/express": "*" } @@ -1791,6 +1855,15 @@ "@types/passport": "*" } }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/pg": { "version": "8.10.9", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", @@ -1874,14 +1947,12 @@ "node_modules/@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", - "dev": true + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/semver": { "version": "7.5.6", @@ -1892,7 +1963,6 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -1902,13 +1972,37 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } }, + "node_modules/@types/xml-crypto": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.6.tgz", + "integrity": "sha512-A6jEW2FxLZo1CXsRWnZHUX2wzR3uDju2Bozt6rDbSmU/W8gkilaVbwFEVN0/NhnUdMVzwYobWtM6bU1QJJFb7Q==", + "dependencies": { + "@types/node": "*", + "xpath": "0.0.27" + } + }, + "node_modules/@types/xml-encryption": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz", + "integrity": "sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.13.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz", @@ -2305,6 +2399,14 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -6699,6 +6801,23 @@ "node": ">=6" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-github": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz", @@ -6836,6 +6955,11 @@ "node": "*" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", @@ -7781,6 +7905,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -9295,8 +9424,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universal-user-agent": { "version": "6.0.1", @@ -10297,6 +10425,83 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xml-crypto": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.0.tgz", + "integrity": "sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "xpath": "0.0.32" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml-crypto/node_modules/xpath": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", + "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/xml-encryption": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.0.2.tgz", + "integrity": "sha512-VxYXPvsWB01/aqVLd6ZMPWZ+qaj0aIdF+cStrVJMcFj3iymwZeI0ABzB3VqMYv48DkSpRhnrXqTUkR34j+UDyg==", + "dependencies": { + "@xmldom/xmldom": "^0.8.5", + "escape-html": "^1.0.3", + "xpath": "0.0.32" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/xml-encryption/node_modules/xpath": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", + "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xpath": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", + "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==", + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend-pg/package.json b/backend-pg/package.json index 0e2a09935..b8352e786 100644 --- a/backend-pg/package.json +++ b/backend-pg/package.json @@ -72,6 +72,7 @@ "@fastify/session": "^10.7.0", "@fastify/swagger": "^8.12.0", "@fastify/swagger-ui": "^1.10.1", + "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^20.0.2", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", diff --git a/backend-pg/src/@types/fastify.d.ts b/backend-pg/src/@types/fastify.d.ts index 37f1616a7..c3de50abe 100644 --- a/backend-pg/src/@types/fastify.d.ts +++ b/backend-pg/src/@types/fastify.d.ts @@ -1,7 +1,10 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-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"; @@ -62,6 +65,8 @@ declare module "fastify" { isCompleted: string; providerAuthToken: string; }; + auditLogInfo: Pick; + ssoConfig: Awaited>; } interface FastifyInstance { @@ -98,6 +103,8 @@ declare module "fastify" { secretApprovalRequest: TSecretApprovalRequestServiceFactory; secretRotation: TSecretRotationServiceFactory; snapshot: TSecretSnapshotServiceFactory; + saml: TSamlConfigServiceFactory; + auditLog: TAuditLogServiceFactory; }; // 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 7bc7c9eb0..cf0bc6af6 100644 --- a/backend-pg/src/@types/knex.d.ts +++ b/backend-pg/src/@types/knex.d.ts @@ -5,6 +5,9 @@ import { TApiKeys, TApiKeysInsert, TApiKeysUpdate, + TAuditLogs, + TAuditLogsInsert, + TAuditLogsUpdate, TAuthTokens, TAuthTokenSessions, TAuthTokenSessionsInsert, @@ -43,6 +46,9 @@ import { TOrganizations, TOrganizationsInsert, TOrganizationsUpdate, + TOrgBots, + TOrgBotsInsert, + TOrgBotsUpdate, TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate, @@ -67,6 +73,9 @@ import { TProjects, TProjectsInsert, TProjectsUpdate, + TSamlConfigs, + TSamlConfigsInsert, + TSamlConfigsUpdate, TSapApprovers, TSapApproversInsert, TSapApproversUpdate, @@ -357,6 +366,13 @@ declare module "knex/types/tables" { TSecretSnapshotFoldersInsert, TSecretSnapshotFoldersUpdate >; + [TableName.SamlConfig]: Knex.CompositeTableType< + TSamlConfigs, + TSamlConfigsInsert, + TSamlConfigsUpdate + >; + [TableName.OrgBot]: Knex.CompositeTableType; + [TableName.AuditLog]: Knex.CompositeTableType; // Junction tables [TableName.JnSecretTag]: Knex.CompositeTableType< TSecretTagJunction, diff --git a/backend-pg/src/db/migrations/20231218103423_secret.ts b/backend-pg/src/db/migrations/20231218103423_secret.ts index c5bd76b44..4334e2bf3 100644 --- a/backend-pg/src/db/migrations/20231218103423_secret.ts +++ b/backend-pg/src/db/migrations/20231218103423_secret.ts @@ -22,7 +22,7 @@ export async function up(knex: Knex): Promise { if (!(await knex.schema.hasTable(TableName.Secret))) { await knex.schema.createTable(TableName.Secret, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); - t.integer("version").defaultTo(1); + t.integer("version").defaultTo(1).notNullable(); t.string("type").notNullable().defaultTo(SecretType.Shared); // t.text("secretKeyHash").notNullable(); // t.text("secretValueHash"); diff --git a/backend-pg/src/db/migrations/20231220052508_secret-version.ts b/backend-pg/src/db/migrations/20231220052508_secret-version.ts index 7e9f97adf..4b515b8e1 100644 --- a/backend-pg/src/db/migrations/20231220052508_secret-version.ts +++ b/backend-pg/src/db/migrations/20231220052508_secret-version.ts @@ -7,7 +7,7 @@ export async function up(knex: Knex): Promise { if (!(await knex.schema.hasTable(TableName.SecretVersion))) { await knex.schema.createTable(TableName.SecretVersion, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); - t.integer("version").defaultTo(1); + t.integer("version").defaultTo(1).notNullable(); t.string("type").notNullable().defaultTo(SecretType.Shared); t.text("secretBlindIndex").notNullable(); t.text("secretKeyCiphertext").notNullable(); diff --git a/backend-pg/src/db/migrations/20240107153439_saml-config.ts b/backend-pg/src/db/migrations/20240107153439_saml-config.ts new file mode 100644 index 000000000..f38bdca07 --- /dev/null +++ b/backend-pg/src/db/migrations/20240107153439_saml-config.ts @@ -0,0 +1,33 @@ +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.SamlConfig))) { + await knex.schema.createTable(TableName.SamlConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("authProvider").notNullable(); + t.boolean("isActive").notNullable(); + t.string("encryptedEntryPoint"); + t.string("entryPointIV"); + t.string("entryPointTag"); + t.string("encryptedIssuer"); + t.string("issuerTag"); + t.string("issuerIV"); + t.string("encryptedCert"); + t.string("certIV"); + t.string("certTag"); + t.timestamps(true, true, true); + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization); + }); + } + + await createOnUpdateTrigger(knex, TableName.SamlConfig); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.SamlConfig); + await dropOnUpdateTrigger(knex, TableName.SamlConfig); +} diff --git a/backend-pg/src/db/migrations/20240107163155_org-bot.ts b/backend-pg/src/db/migrations/20240107163155_org-bot.ts new file mode 100644 index 000000000..6e4365f9d --- /dev/null +++ b/backend-pg/src/db/migrations/20240107163155_org-bot.ts @@ -0,0 +1,35 @@ +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.OrgBot))) { + await knex.schema.createTable(TableName.OrgBot, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.text("publicKey").notNullable(); + t.text("encryptedSymmetricKey").notNullable(); + t.text("symmetricKeyIV").notNullable(); + t.text("symmetricKeyTag").notNullable(); + t.string("symmetricKeyAlgorithm").notNullable(); + t.string("symmetricKeyKeyEncoding").notNullable(); + t.text("encryptedPrivateKey").notNullable(); + t.text("privateKeyIV").notNullable(); + t.text("privateKeyTag").notNullable(); + t.string("privateKeyAlgorithm").notNullable(); + t.string("privateKeyKeyEncoding").notNullable(); + // one to one relationship + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.OrgBot); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.OrgBot); + await dropOnUpdateTrigger(knex, TableName.OrgBot); +} diff --git a/backend-pg/src/db/migrations/20240108134148_audit-log.ts b/backend-pg/src/db/migrations/20240108134148_audit-log.ts new file mode 100644 index 000000000..b271f9ab3 --- /dev/null +++ b/backend-pg/src/db/migrations/20240108134148_audit-log.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.AuditLog))) { + await knex.schema.createTable(TableName.AuditLog, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("actor").notNullable(); + t.jsonb("actorMetadata").notNullable(); + t.string("ipAddress"); + t.string("eventType").notNullable(); + t.jsonb("eventMetadata"); + t.string("userAgent"); + t.string("userAgentType"); + t.datetime("expiresAt"); + t.timestamps(true, true, true); + // no trigger needed as this collection is append only + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.uuid("projectId"); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AuditLog); +} diff --git a/backend-pg/src/db/schemas/audit-logs.ts b/backend-pg/src/db/schemas/audit-logs.ts new file mode 100644 index 000000000..2f3c472b3 --- /dev/null +++ b/backend-pg/src/db/schemas/audit-logs.ts @@ -0,0 +1,28 @@ +// 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 AuditLogsSchema = z.object({ + id: z.string().uuid(), + actor: z.string(), + actorMetadata: z.unknown(), + ipAddress: z.string().nullable().optional(), + eventType: z.string(), + eventMetadata: z.unknown().nullable().optional(), + userAgent: z.string().nullable().optional(), + userAgentType: z.string().nullable().optional(), + expiresAt: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid().nullable().optional(), + projectId: z.string().uuid().nullable().optional(), +}); + +export type TAuditLogs = z.infer; +export type TAuditLogsInsert = Omit; +export type TAuditLogsUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/index.ts b/backend-pg/src/db/schemas/index.ts index 8ff1abe3f..db44c8100 100644 --- a/backend-pg/src/db/schemas/index.ts +++ b/backend-pg/src/db/schemas/index.ts @@ -1,4 +1,5 @@ export * from "./api-keys"; +export * from "./audit-logs"; export * from "./auth-token-sessions"; export * from "./auth-tokens"; export * from "./backup-private-key"; @@ -12,6 +13,7 @@ export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; export * from "./models"; +export * from "./org-bots"; export * from "./org-memberships"; export * from "./org-roles"; export * from "./organizations"; @@ -23,6 +25,7 @@ export * from "./project-roles"; export * from "./projects"; export * from "./sa-request-secret-tags"; export * from "./sa-request-secrets"; +export * from "./saml-configs"; export * from "./sap-approvers"; export * from "./sar-reviewers"; export * from "./secret-approval-policies"; diff --git a/backend-pg/src/db/schemas/models.ts b/backend-pg/src/db/schemas/models.ts index f68d2eb1b..1554d0ebd 100644 --- a/backend-pg/src/db/schemas/models.ts +++ b/backend-pg/src/db/schemas/models.ts @@ -9,6 +9,7 @@ export enum TableName { Organization = "organizations", OrgMembership = "org_memberships", OrgRoles = "org_roles", + OrgBot = "org_bots", IncidentContact = "incident_contacts", UserAction = "user_actions", SuperAdmin = "super_admin", @@ -47,6 +48,8 @@ export enum TableName { SarSecretTag = "sa_request_secret_tags", SecretRotation = "secret_rotations", SecretRotationOutput = "secret_rotation_outputs", + SamlConfig = "saml_configs", + AuditLog = "audit_logs", // junction tables JnSecretTag = "secret_tag_junction", JnSecretVersionTag = "secret_version_tag_junction" diff --git a/backend-pg/src/db/schemas/org-bots.ts b/backend-pg/src/db/schemas/org-bots.ts new file mode 100644 index 000000000..400ab19c8 --- /dev/null +++ b/backend-pg/src/db/schemas/org-bots.ts @@ -0,0 +1,31 @@ +// 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 OrgBotsSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + publicKey: z.string(), + encryptedSymmetricKey: z.string(), + symmetricKeyIV: z.string(), + symmetricKeyTag: z.string(), + symmetricKeyAlgorithm: z.string(), + symmetricKeyKeyEncoding: z.string(), + encryptedPrivateKey: z.string(), + privateKeyIV: z.string(), + privateKeyTag: z.string(), + privateKeyAlgorithm: z.string(), + privateKeyKeyEncoding: z.string(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +export type TOrgBots = z.infer; +export type TOrgBotsInsert = Omit; +export type TOrgBotsUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/saml-configs.ts b/backend-pg/src/db/schemas/saml-configs.ts new file mode 100644 index 000000000..392fb2bef --- /dev/null +++ b/backend-pg/src/db/schemas/saml-configs.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 SamlConfigsSchema = z.object({ + id: z.string().uuid(), + authProvider: z.string(), + isActive: z.boolean(), + encryptedEntryPoint: z.string().nullable().optional(), + entryPointIV: z.string().nullable().optional(), + entryPointTag: z.string().nullable().optional(), + encryptedIssuer: z.string().nullable().optional(), + issuerTag: z.string().nullable().optional(), + issuerIV: z.string().nullable().optional(), + encryptedCert: z.string().nullable().optional(), + certIV: z.string().nullable().optional(), + certTag: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid(), +}); + +export type TSamlConfigs = z.infer; +export type TSamlConfigsInsert = Omit; +export type TSamlConfigsUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/secret-versions.ts b/backend-pg/src/db/schemas/secret-versions.ts index ee64861fc..7f3cc40f6 100644 --- a/backend-pg/src/db/schemas/secret-versions.ts +++ b/backend-pg/src/db/schemas/secret-versions.ts @@ -9,7 +9,7 @@ import { TImmutableDBKeys } from "./models"; export const SecretVersionsSchema = z.object({ id: z.string().uuid(), - version: z.number().default(1).nullable().optional(), + version: z.number().default(1), type: z.string().default("shared"), secretBlindIndex: z.string(), secretKeyCiphertext: z.string(), diff --git a/backend-pg/src/db/schemas/secrets.ts b/backend-pg/src/db/schemas/secrets.ts index 18ae17a7e..e08ae8592 100644 --- a/backend-pg/src/db/schemas/secrets.ts +++ b/backend-pg/src/db/schemas/secrets.ts @@ -9,7 +9,7 @@ import { TImmutableDBKeys } from "./models"; export const SecretsSchema = z.object({ id: z.string().uuid(), - version: z.number().default(1).nullable().optional(), + version: z.number().default(1), type: z.string().default("shared"), secretBlindIndex: z.string(), secretKeyCiphertext: z.string(), diff --git a/backend-pg/src/ee/routes/v1/index.ts b/backend-pg/src/ee/routes/v1/index.ts index 6a4b22f2a..4d8d65ba0 100644 --- a/backend-pg/src/ee/routes/v1/index.ts +++ b/backend-pg/src/ee/routes/v1/index.ts @@ -1,6 +1,7 @@ import { registerOrgRoleRouter } from "./org-role-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRouter } from "./project-router"; +import { registerSamlRouter } from "./saml-router"; import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router"; import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router"; import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; @@ -25,5 +26,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerSecretRotationProviderRouter, { prefix: "/secret-rotation-providers" }); + await server.register(registerSamlRouter, { prefix: "/sso" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); }; diff --git a/backend-pg/src/ee/routes/v1/project-router.ts b/backend-pg/src/ee/routes/v1/project-router.ts index bda6cfff8..f5b67042f 100644 --- a/backend-pg/src/ee/routes/v1/project-router.ts +++ b/backend-pg/src/ee/routes/v1/project-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; -import { SecretSnapshotsSchema } from "@app/db/schemas"; +import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas"; +import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -65,4 +66,74 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { count }; } }); + + server.route({ + method: "GET", + url: "/:workspaceId/audit-logs", + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + querystring: z.object({ + eventType: z.nativeEnum(EventType).optional(), + userAgentType: z.nativeEnum(UserAgentType).optional(), + startDate: z.string().datetime().optional(), + endDate: z.string().datetime().optional(), + offset: z.coerce.number().default(0), + limit: z.coerce.number().default(20), + actor: z.string().optional() + }), + response: { + 200: z.object({ + auditLogs: AuditLogsSchema.omit({ + eventMetadata: true, + eventType: true, + actor: true, + actorMetadata: true + }) + .merge( + z.object({ + event: z.object({ + type: z.string(), + metadata: z.any() + }), + actor: z.object({ + type: z.string(), + metadata: z.any() + }) + }) + ) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const auditLogs = await server.services.auditLog.listProjectAuditLogs({ + actorId: req.permission.id, + projectId: req.params.workspaceId, + ...req.query, + auditLogActor: req.query.actor, + actor: req.permission.type + }); + return { auditLogs }; + } + }); + + server.route({ + method: "GET", + url: "/:workspaceId/audit-logs/filters/actors", + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + actors: z.string().array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async () => ({ actors: [] }) + }); }; diff --git a/backend-pg/src/ee/routes/v1/saml-router.ts b/backend-pg/src/ee/routes/v1/saml-router.ts new file mode 100644 index 000000000..e33308167 --- /dev/null +++ b/backend-pg/src/ee/routes/v1/saml-router.ts @@ -0,0 +1,237 @@ +import { Authenticator } from "@fastify/passport"; +import fastifySession from "@fastify/session"; +import { MultiSamlStrategy } from "@node-saml/passport-saml"; +import { z } from "zod"; + +import { SamlConfigsSchema } from "@app/db/schemas"; +import { SamlProviders } from "@app/ee/services/saml-config/saml-config-types"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +type TSAMLConfig = { + callbackUrl: string; + entryPoint: string; + issuer: string; + cert: string; + audience: string; + wantAuthnResponseSigned?: boolean; +}; + +export const registerSamlRouter = async (server: FastifyZodProvider) => { + const appCfg = getConfig(); + const passport = new Authenticator({ key: "saml", userProperty: "passportUser" }); + await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY }); + await server.register(passport.initialize()); + await server.register(passport.secureSession()); + server.decorateRequest("ssoConfig", null); + passport.use( + new MultiSamlStrategy( + { + passReqToCallback: true, + getSamlOptions: async (req, done) => { + const { ssoIdentifier } = req.params; + if (!ssoIdentifier) throw new BadRequestError({ message: "Missing sso identitier" }); + + const ssoConfig = await server.services.saml.getSaml({ + type: "ssoId", + id: ssoIdentifier + }); + if (!ssoConfig) throw new BadRequestError({ message: "SSO config not found" }); + + const samlConfig: TSAMLConfig = { + callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoIdentifier}`, + entryPoint: ssoConfig.entryPoint, + issuer: ssoConfig.issuer, + cert: ssoConfig.cert, + audience: appCfg.SITE_URL || "" + }; + if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) { + samlConfig.wantAuthnResponseSigned = false; + } + if (ssoConfig.authProvider === SamlProviders.AZURE_SAML) { + if (req.body.RelayState && JSON.parse(req.body.RelayState).spIntiaited) { + samlConfig.audience = `spn:${ssoConfig.issuer}`; + } + } + req.ssoConfig = ssoConfig; + done(null, samlConfig); + } + }, + async (req, profile, cb) => { + console.log(req.ssoConfig); + console.log(profile); + try { + const serverCfg = server.services.superAdmin.getServerCfg(); + if (!profile) throw new BadRequestError({ message: "Missing profile" }); + const { email, firstName } = profile; + if (!email || !firstName) + throw new BadRequestError({ message: "Invalid request. Missing email or first name" }); + + const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ + email, + firstName: profile.firstName as string, + lastName: profile.lastName as string, + isSignupAllowed: Boolean(serverCfg.allowSignUp), + relayState: (req.body as { RelayState?: string }).RelayState, + authProvider: req.ssoConfig.authProvider, + orgId: req.ssoConfig.orgId + }); + cb(null, { isUserCompleted, providerAuthToken }); + } catch (error) { + cb(null, {}); + } + }, + () => {} + ) + ); + + server.route({ + url: "/redirect/saml2/:ssoIdentifier", + method: "GET", + schema: { + params: z.object({ + ssoIdentifier: z.string().trim() + }), + querystring: z.object({ + callback_port: z.string().optional() + }) + }, + preValidation: (req, res) => + ( + passport.authenticate("saml", { + failureRedirect: "/", + additionalParams: { + RelayState: JSON.stringify({ + spInitiated: true, + callbackPort: req.query.callback_port ?? "" + }) + } + } as any) as any + )(req, res), + handler: () => {} + }); + + server.route({ + url: "/saml2/:ssoIdentifier", + method: "GET", + schema: { + params: z.object({ + ssoIdentifier: z.string().trim() + }) + }, + preValidation: passport.authenticate("saml", { + session: false, + failureFlash: true, + failureRedirect: "/login/provider/error" + // this is due to zod type difference + }) as any, + handler: (req, res) => { + if (req.passportUser.isCompleted) { + return res.redirect( + `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent( + req.passportUser.providerAuthToken + )}` + ); + } + return res.redirect( + `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent( + req.passportUser.providerAuthToken + )}` + ); + } + }); + + server.route({ + url: "/config", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + querystring: z.object({ + organizationId: z.string().trim() + }), + response: { + 200: z + .object({ + id: z.string(), + organization: z.string(), + orgId: z.string(), + authProvider: z.string(), + isActive: z.boolean(), + entryPoint: z.string(), + issuer: z.string(), + cert: z.string() + }) + .optional() + } + }, + handler: async (req) => { + const saml = await server.services.saml.getSaml({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.query.organizationId, + type: "org" + }); + return saml; + } + }); + + server.route({ + url: "/config", + method: "POST", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z.object({ + organizationId: z.string(), + authProvider: z.nativeEnum(SamlProviders), + isActive: z.boolean(), + entryPoint: z.string(), + issuer: z.string(), + cert: z.string() + }), + response: { + 200: SamlConfigsSchema + } + }, + handler: async (req) => { + const saml = await server.services.saml.createSamlCfg({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.body.organizationId, + ...req.body + }); + return saml; + } + }); + + server.route({ + url: "/config", + method: "PATCH", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z + .object({ + authProvider: z.nativeEnum(SamlProviders), + isActive: z.boolean(), + entryPoint: z.string(), + issuer: z.string(), + cert: z.string() + }) + .partial() + .merge(z.object({ organizationId: z.string() })), + response: { + 200: SamlConfigsSchema + } + }, + handler: async (req) => { + const saml = await server.services.saml.updateSamlCfg({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.body.organizationId, + ...req.body + }); + return saml; + } + }); +}; 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 index 71fe9fb41..9cb4c2d5b 100644 --- a/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend-pg/src/ee/routes/v1/secret-approval-request-router.ts @@ -7,6 +7,7 @@ import { SecretsSchema, SecretVersionsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState @@ -230,6 +231,23 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv approvalId: req.params.id, status: req.body.status }); + + const isClosing = approval.status === RequestState.Closed; + await server.services.auditLog.createAuditLog({ + projectId: approval.projectId, + ...req.auditLogInfo, + event: { + type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED, + metadata: { + [isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: + approval.statusChangeBy as string, + secretApprovalRequestId: approval.id, + secretApprovalRequestSlug: approval.slug + } as any + // akhilmhdh: had to apply any to avoid ts issue with this + } + }); + return { approval }; } }); diff --git a/backend-pg/src/ee/services/audit-log/audit-log-dal.ts b/backend-pg/src/ee/services/audit-log/audit-log-dal.ts new file mode 100644 index 000000000..ee7716e65 --- /dev/null +++ b/backend-pg/src/ee/services/audit-log/audit-log-dal.ts @@ -0,0 +1,61 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, stripUndefinedInWhere } from "@app/lib/knex"; + +export type TAuditLogDalFactory = ReturnType; + +type TFindQuery = { + actor?: string; + projectId?: string; + orgId?: string; + eventType?: string; + startDate?: string; + endDate?: string; + userAgentType?: string; + limit?: number; + offset?: number; +}; + +export const auditLogDalFactory = (db: TDbClient) => { + const auditLogOrm = ormify(db, TableName.AuditLog); + + const find = async ( + { + orgId, + projectId, + userAgentType, + startDate, + endDate, + limit = 20, + offset = 0, + actor, + eventType + }: TFindQuery, + tx?: Knex + ) => { + const sqlQuery = (tx || db)(TableName.AuditLog) + .where( + stripUndefinedInWhere({ + projectId, + orgId, + eventType, + actor, + userAgentType + }) + ) + .limit(limit) + .offset(offset); + if (startDate) { + sqlQuery.where("createdAt", ">=", startDate); + } + if (endDate) { + sqlQuery.where("createdAt", "<=", endDate); + } + const docs = await sqlQuery; + return docs; + }; + + return { ...auditLogOrm, find }; +}; diff --git a/backend-pg/src/ee/services/audit-log/audit-log-queue.ts b/backend-pg/src/ee/services/audit-log/audit-log-queue.ts new file mode 100644 index 000000000..b110a5cd8 --- /dev/null +++ b/backend-pg/src/ee/services/audit-log/audit-log-queue.ts @@ -0,0 +1,46 @@ +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { TAuditLogDalFactory } from "./audit-log-dal"; +import { TCreateAuditLogDTO } from "./audit-log-types"; + +type TAuditLogQueueServiceFactoryDep = { + auditLogDal: TAuditLogDalFactory; + queueService: TQueueServiceFactory; +}; + +export type TAuditLogQueueServiceFactory = ReturnType; + +export const auditLogQueueServiceFactory = ({ + auditLogDal, + queueService +}: TAuditLogQueueServiceFactoryDep) => { + const pushToLog = async (data: TCreateAuditLogDTO) => { + await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { + removeOnFail: { + count: 5 + }, + removeOnComplete: true + }); + }; + + queueService.start(QueueName.AuditLog, async (job) => { + const { actor, orgId, event, ipAddress, projectId, userAgent, userAgentType } = job.data; + const MS_IN_DAY = 24 * 60 * 60 * 1000; + await auditLogDal.create({ + actor: actor.type, + actorMetadata: actor.metadata, + userAgent, + projectId, + ipAddress, + orgId, + eventType: event.type, + expiresAt: new Date(Date.now() + 30 * MS_IN_DAY), + eventMetadata: event.metadata, + userAgentType + }); + }); + + return { + pushToLog + }; +}; diff --git a/backend-pg/src/ee/services/audit-log/audit-log-service.ts b/backend-pg/src/ee/services/audit-log/audit-log-service.ts new file mode 100644 index 000000000..796f0f8e7 --- /dev/null +++ b/backend-pg/src/ee/services/audit-log/audit-log-service.ts @@ -0,0 +1,73 @@ +import { ForbiddenError } from "@casl/ability"; + +import { BadRequestError } from "@app/lib/errors"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TAuditLogDalFactory } from "./audit-log-dal"; +import { TAuditLogQueueServiceFactory } from "./audit-log-queue"; +import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-log-types"; + +type TAuditLogServiceFactoryDep = { + auditLogDal: TAuditLogDalFactory; + permissionService: Pick; + auditLogQueue: TAuditLogQueueServiceFactory; +}; + +export type TAuditLogServiceFactory = ReturnType; + +export const auditLogServiceFactory = ({ + auditLogDal, + auditLogQueue, + permissionService +}: TAuditLogServiceFactoryDep) => { + const listProjectAuditLogs = async ({ + userAgentType, + eventType, + offset, + limit, + endDate, + startDate, + actor, + actorId, + projectId, + auditLogActor + }: TListProjectAuditLogDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.AuditLogs + ); + const auditLogs = await auditLogDal.find({ + startDate, + endDate, + limit, + offset, + eventType, + userAgentType, + actor: auditLogActor, + projectId + }); + return auditLogs.map( + ({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({ + ...el, + event: { type: logEventType, metadata: eventMetadata }, + actor: { type: eActor, metadata: actorMetadata } + }) + ); + }; + + const createAuditLog = async (data: TCreateAuditLogDTO) => { + // add all cases in which project id or org id cannot be added + if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { + if (!data.projectId && !data.orgId) + throw new BadRequestError({ message: "Must either project id or org id" }); + } + return auditLogQueue.pushToLog(data); + }; + + return { + createAuditLog, + listProjectAuditLogs + }; +}; diff --git a/backend-pg/src/ee/services/audit-log/audit-log-types.ts b/backend-pg/src/ee/services/audit-log/audit-log-types.ts new file mode 100644 index 000000000..4b2dc2fcf --- /dev/null +++ b/backend-pg/src/ee/services/audit-log/audit-log-types.ts @@ -0,0 +1,674 @@ +import { TProjectPermission } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; + +export type TListProjectAuditLogDTO = { + auditLogActor?: string; + projectId: string; + eventType?: string; + startDate?: string; + endDate?: string; + userAgentType?: string; + limit?: number; + offset?: number; +} & TProjectPermission; + +export type TCreateAuditLogDTO = { + event: Event; + actor: UserActor | IdentityActor | ServiceActor; + orgId?: string; + projectId?: string; +} & BaseAuthData; + +interface BaseAuthData { + ipAddress?: string; + userAgent?: string; + userAgentType?: UserAgentType; +} + +export enum UserAgentType { + WEB = "web", + CLI = "cli", + K8_OPERATOR = "k8-operator", + TERRAFORM = "terraform", + OTHER = "other", + PYTHON_SDK = "InfisicalPythonSDK", + NODE_SDK = "InfisicalNodeSDK" +} + +export enum EventType { + GET_SECRETS = "get-secrets", + GET_SECRET = "get-secret", + REVEAL_SECRET = "reveal-secret", + CREATE_SECRET = "create-secret", + CREATE_SECRETS = "create-secrets", + UPDATE_SECRET = "update-secret", + UPDATE_SECRETS = "update-secrets", + DELETE_SECRET = "delete-secret", + DELETE_SECRETS = "delete-secrets", + GET_WORKSPACE_KEY = "get-workspace-key", + AUTHORIZE_INTEGRATION = "authorize-integration", + UNAUTHORIZE_INTEGRATION = "unauthorize-integration", + CREATE_INTEGRATION = "create-integration", + DELETE_INTEGRATION = "delete-integration", + ADD_TRUSTED_IP = "add-trusted-ip", + UPDATE_TRUSTED_IP = "update-trusted-ip", + DELETE_TRUSTED_IP = "delete-trusted-ip", + CREATE_SERVICE_TOKEN = "create-service-token", // v2 + DELETE_SERVICE_TOKEN = "delete-service-token", // v2 + CREATE_IDENTITY = "create-identity", + UPDATE_IDENTITY = "update-identity", + DELETE_IDENTITY = "delete-identity", + LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth", + ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth", + UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", + GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth", + CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", + REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", + GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", + CREATE_ENVIRONMENT = "create-environment", + UPDATE_ENVIRONMENT = "update-environment", + DELETE_ENVIRONMENT = "delete-environment", + ADD_WORKSPACE_MEMBER = "add-workspace-member", + ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members", + REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + CREATE_FOLDER = "create-folder", + UPDATE_FOLDER = "update-folder", + DELETE_FOLDER = "delete-folder", + CREATE_WEBHOOK = "create-webhook", + UPDATE_WEBHOOK_STATUS = "update-webhook-status", + DELETE_WEBHOOK = "delete-webhook", + GET_SECRET_IMPORTS = "get-secret-imports", + CREATE_SECRET_IMPORT = "create-secret-import", + UPDATE_SECRET_IMPORT = "update-secret-import", + DELETE_SECRET_IMPORT = "delete-secret-import", + UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", + UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions", + SECRET_APPROVAL_MERGED = "secret-approval-merged", + SECRET_APPROVAL_REQUEST = "secret-approval-request", + SECRET_APPROVAL_CLOSED = "secret-approval-closed", + SECRET_APPROVAL_REOPENED = "secret-approval-reopened" +} + +interface UserActorMetadata { + userId: string; + email: string; +} + +interface ServiceActorMetadata { + serviceId: string; + name: string; +} + +interface IdentityActorMetadata { + identityId: string; + name: string; +} + +export interface UserActor { + type: ActorType.USER; + metadata: UserActorMetadata; +} + +export interface ServiceActor { + type: ActorType.SERVICE; + metadata: ServiceActorMetadata; +} + +export interface IdentityActor { + type: ActorType.IDENTITY; + metadata: IdentityActorMetadata; +} + +export type Actor = UserActor | ServiceActor | IdentityActor; + +interface GetSecretsEvent { + type: EventType.GET_SECRETS; + metadata: { + environment: string; + secretPath: string; + numberOfSecrets: number; + }; +} + +interface GetSecretEvent { + type: EventType.GET_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + }; +} + +interface CreateSecretEvent { + type: EventType.CREATE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + }; +} + +interface CreateSecretBatchEvent { + type: EventType.CREATE_SECRETS; + metadata: { + environment: string; + secretPath: string; + secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>; + }; +} + +interface UpdateSecretEvent { + type: EventType.UPDATE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + }; +} + +interface UpdateSecretBatchEvent { + type: EventType.UPDATE_SECRETS; + metadata: { + environment: string; + secretPath: string; + secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>; + }; +} + +interface DeleteSecretEvent { + type: EventType.DELETE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + }; +} + +interface DeleteSecretBatchEvent { + type: EventType.DELETE_SECRETS; + metadata: { + environment: string; + secretPath: string; + secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>; + }; +} + +interface GetWorkspaceKeyEvent { + type: EventType.GET_WORKSPACE_KEY; + metadata: { + keyId: string; + }; +} + +interface AuthorizeIntegrationEvent { + type: EventType.AUTHORIZE_INTEGRATION; + metadata: { + integration: string; + }; +} + +interface UnauthorizeIntegrationEvent { + type: EventType.UNAUTHORIZE_INTEGRATION; + metadata: { + integration: string; + }; +} + +interface CreateIntegrationEvent { + type: EventType.CREATE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + url?: string; + app?: string; + appId?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + targetService?: string; + targetServiceId?: string; + path?: string; + region?: string; + }; +} + +interface DeleteIntegrationEvent { + type: EventType.DELETE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + url?: string; + app?: string; + appId?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + targetService?: string; + targetServiceId?: string; + path?: string; + region?: string; + }; +} + +interface AddTrustedIPEvent { + type: EventType.ADD_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + }; +} + +interface UpdateTrustedIPEvent { + type: EventType.UPDATE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + }; +} + +interface DeleteTrustedIPEvent { + type: EventType.DELETE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + }; +} + +interface CreateServiceTokenEvent { + type: EventType.CREATE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + }; +} + +interface DeleteServiceTokenEvent { + type: EventType.DELETE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + }; +} + +interface CreateIdentityEvent { + // note: currently not logging org-role + type: EventType.CREATE_IDENTITY; + metadata: { + identityId: string; + name: string; + }; +} + +interface UpdateIdentityEvent { + type: EventType.UPDATE_IDENTITY; + metadata: { + identityId: string; + name?: string; + }; +} + +interface DeleteIdentityEvent { + type: EventType.DELETE_IDENTITY; + metadata: { + identityId: string; + }; +} + +interface LoginIdentityUniversalAuthEvent { + type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + identityUniversalAuthId: string; + clientSecretId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityUniversalAuthEvent { + type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + clientSecretTrustedIps: Array; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityUniversalAuthEvent { + type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + clientSecretTrustedIps?: Array; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityUniversalAuthEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + }; +} + +interface CreateIdentityUniversalAuthClientSecretEvent { + type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET; + metadata: { + identityId: string; + clientSecretId: string; + }; +} + +interface GetIdentityUniversalAuthClientSecretsEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS; + metadata: { + identityId: string; + }; +} + +interface RevokeIdentityUniversalAuthClientSecretEvent { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET; + metadata: { + identityId: string; + clientSecretId: string; + }; +} + +interface CreateEnvironmentEvent { + type: EventType.CREATE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + }; +} + +interface UpdateEnvironmentEvent { + type: EventType.UPDATE_ENVIRONMENT; + metadata: { + oldName: string; + newName: string; + oldSlug: string; + newSlug: string; + }; +} + +interface DeleteEnvironmentEvent { + type: EventType.DELETE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + }; +} + +interface AddWorkspaceMemberEvent { + type: EventType.ADD_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + }; +} + +interface AddBatchWorkspaceMemberEvent { + type: EventType.ADD_BATCH_WORKSPACE_MEMBER; + metadata: Array<{ + userId: string; + email: string; + }>; +} + +interface RemoveWorkspaceMemberEvent { + type: EventType.REMOVE_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + }; +} + +interface CreateFolderEvent { + type: EventType.CREATE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + }; +} + +interface UpdateFolderEvent { + type: EventType.UPDATE_FOLDER; + metadata: { + environment: string; + folderId: string; + oldFolderName: string; + newFolderName: string; + folderPath: string; + }; +} + +interface DeleteFolderEvent { + type: EventType.DELETE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + }; +} + +interface CreateWebhookEvent { + type: EventType.CREATE_WEBHOOK; + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + }; +} + +interface UpdateWebhookStatusEvent { + type: EventType.UPDATE_WEBHOOK_STATUS; + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + }; +} + +interface DeleteWebhookEvent { + type: EventType.DELETE_WEBHOOK; + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + }; +} + +interface GetSecretImportsEvent { + type: EventType.GET_SECRET_IMPORTS; + metadata: { + environment: string; + folderId: string; + numberOfImports: number; + }; +} + +interface CreateSecretImportEvent { + type: EventType.CREATE_SECRET_IMPORT; + metadata: { + secretImportId: string; + folderId: string; + importFromEnvironment: string; + importFromSecretPath: string; + importToEnvironment: string; + importToSecretPath: string; + }; +} + +interface UpdateSecretImportEvent { + type: EventType.UPDATE_SECRET_IMPORT; + metadata: { + secretImportId: string; + folderId: string; + importToEnvironment: string; + importToSecretPath: string; + position: number; + orderBefore?: { + environment: string; + secretPath: string; + }[]; + orderAfter?: { + environment: string; + secretPath: string; + }[]; + }; +} + +interface DeleteSecretImportEvent { + type: EventType.DELETE_SECRET_IMPORT; + metadata: { + secretImportId: string; + folderId: string; + importFromEnvironment: string; + importFromSecretPath: string; + importToEnvironment: string; + importToSecretPath: string; + }; +} + +interface UpdateUserRole { + type: EventType.UPDATE_USER_WORKSPACE_ROLE; + metadata: { + userId: string; + email: string; + oldRole: string; + newRole: string; + }; +} + +interface UpdateUserDeniedPermissions { + type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS; + metadata: { + userId: string; + email: string; + deniedPermissions: { + environmentSlug: string; + ability: string; + }[]; + }; +} +interface SecretApprovalMerge { + type: EventType.SECRET_APPROVAL_MERGED; + metadata: { + mergedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} + +interface SecretApprovalClosed { + type: EventType.SECRET_APPROVAL_CLOSED; + metadata: { + closedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} + +interface SecretApprovalReopened { + type: EventType.SECRET_APPROVAL_REOPENED; + metadata: { + reopenedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} + +interface SecretApprovalRequest { + type: EventType.SECRET_APPROVAL_REQUEST; + metadata: { + committedBy: string; + secretApprovalRequestSlug: string; + secretApprovalRequestId: string; + }; +} + +export type Event = + | GetSecretsEvent + | GetSecretEvent + | CreateSecretEvent + | CreateSecretBatchEvent + | UpdateSecretEvent + | UpdateSecretBatchEvent + | DeleteSecretEvent + | DeleteSecretBatchEvent + | GetWorkspaceKeyEvent + | AuthorizeIntegrationEvent + | UnauthorizeIntegrationEvent + | CreateIntegrationEvent + | DeleteIntegrationEvent + | AddTrustedIPEvent + | UpdateTrustedIPEvent + | DeleteTrustedIPEvent + | CreateServiceTokenEvent + | DeleteServiceTokenEvent + | CreateIdentityEvent + | UpdateIdentityEvent + | DeleteIdentityEvent + | LoginIdentityUniversalAuthEvent + | AddIdentityUniversalAuthEvent + | UpdateIdentityUniversalAuthEvent + | GetIdentityUniversalAuthEvent + | CreateIdentityUniversalAuthClientSecretEvent + | GetIdentityUniversalAuthClientSecretsEvent + | RevokeIdentityUniversalAuthClientSecretEvent + | CreateEnvironmentEvent + | UpdateEnvironmentEvent + | DeleteEnvironmentEvent + | AddWorkspaceMemberEvent + | AddBatchWorkspaceMemberEvent + | RemoveWorkspaceMemberEvent + | CreateFolderEvent + | UpdateFolderEvent + | DeleteFolderEvent + | CreateWebhookEvent + | UpdateWebhookStatusEvent + | DeleteWebhookEvent + | GetSecretImportsEvent + | CreateSecretImportEvent + | UpdateSecretImportEvent + | DeleteSecretImportEvent + | UpdateUserRole + | UpdateUserDeniedPermissions + | SecretApprovalMerge + | SecretApprovalClosed + | SecretApprovalRequest + | SecretApprovalReopened; diff --git a/backend-pg/src/ee/services/saml-config/saml-config-dal.ts b/backend-pg/src/ee/services/saml-config/saml-config-dal.ts new file mode 100644 index 000000000..1fa414afe --- /dev/null +++ b/backend-pg/src/ee/services/saml-config/saml-config-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TSamlConfigDalFactory = ReturnType; + +export const samlConfigDalFactory = (db: TDbClient) => { + const samlCfgOrm = ormify(db, TableName.SamlConfig); + return samlCfgOrm; +}; diff --git a/backend-pg/src/ee/services/saml-config/saml-config-service.ts b/backend-pg/src/ee/services/saml-config/saml-config-service.ts new file mode 100644 index 000000000..0363f38fc --- /dev/null +++ b/backend-pg/src/ee/services/saml-config/saml-config-service.ts @@ -0,0 +1,348 @@ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +import { + OrgMembershipRole, + OrgMembershipStatus, + SecretKeyEncoding, + TSamlConfigs, + TSamlConfigsUpdate +} from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; +import { + decryptSymmetric, + encryptSymmetric, + infisicalSymmetricDecrypt +} from "@app/lib/crypto/encryption"; +import { BadRequestError } from "@app/lib/errors"; +import { AuthTokenType } from "@app/services/auth/auth-type"; +import { TOrgBotDalFactory } from "@app/services/org/org-bot-dal"; +import { TOrgDalFactory } from "@app/services/org/org-dal"; +import { TUserDalFactory } from "@app/services/user/user-dal"; + +import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TSamlConfigDalFactory } from "./saml-config-dal"; +import { + SamlProviders, + TCreateSamlCfgDTO, + TGetSamlCfgDTO, + TSamlLoginDTO, + TUpdateSamlCfgDTO +} from "./saml-config-types"; + +type TSamlConfigServiceFactoryDep = { + samlConfigDal: TSamlConfigDalFactory; + userDal: Pick; + orgDal: Pick< + TOrgDalFactory, + "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" + >; + orgBotDal: Pick; + permissionService: Pick; +}; + +export type TSamlConfigServiceFactory = ReturnType; + +export const samlConfigServiceFactory = ({ + samlConfigDal, + orgBotDal, + orgDal, + userDal, + permissionService +}: TSamlConfigServiceFactoryDep) => { + const createSamlCfg = async ({ + cert, + actor, + orgId, + issuer, + actorId, + isActive, + entryPoint, + authProvider + }: TCreateSamlCfgDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Sso + ); + + // TODO(akhilmhdh-pg): licence check + + const orgBot = await orgBotDal.findOne({ orgId }); + if (!orgBot) + throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { + ciphertext: encryptedEntryPoint, + iv: entryPointIV, + tag: entryPointTag + } = encryptSymmetric(entryPoint, key); + const { + ciphertext: encryptedIssuer, + iv: issuerIV, + tag: issuerTag + } = encryptSymmetric(issuer, key); + + const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); + const samlConfig = await samlConfigDal.create({ + orgId, + authProvider, + isActive, + encryptedEntryPoint, + entryPointIV, + entryPointTag, + encryptedIssuer, + issuerIV, + issuerTag, + encryptedCert, + certIV, + certTag + }); + return samlConfig; + }; + + const updateSamlCfg = async ({ + orgId, + actor, + cert, + actorId, + issuer, + isActive, + entryPoint, + authProvider + }: TUpdateSamlCfgDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Sso + ); + const updateQuery: TSamlConfigsUpdate = { authProvider, isActive }; + const orgBot = await orgBotDal.findOne({ orgId }); + if (!orgBot) + throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + if (entryPoint) { + const { + ciphertext: encryptedEntryPoint, + iv: entryPointIV, + tag: entryPointTag + } = encryptSymmetric(entryPoint, key); + updateQuery.encryptedEntryPoint = encryptedEntryPoint; + updateQuery.entryPointIV = entryPointIV; + updateQuery.entryPointTag = entryPointTag; + } + if (issuer) { + const { + ciphertext: encryptedIssuer, + iv: issuerIV, + tag: issuerTag + } = encryptSymmetric(issuer, key); + updateQuery.encryptedIssuer = encryptedIssuer; + updateQuery.issuerIV = issuerIV; + updateQuery.issuerTag = issuerTag; + } + if (cert) { + const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); + updateQuery.encryptedCert = encryptedCert; + updateQuery.certIV = certIV; + updateQuery.certTag = certTag; + } + const [ssoConfig] = await samlConfigDal.update({ orgId }, updateQuery); + return ssoConfig; + }; + + const getSaml = async (dto: TGetSamlCfgDTO) => { + let ssoConfig: TSamlConfigs | undefined; + if (dto.type === "org") { + ssoConfig = await samlConfigDal.findOne({ orgId: dto.orgId }); + if (!ssoConfig) return; + } else if (dto.type === "ssoId") { + ssoConfig = await samlConfigDal.findById(dto.id); + } + if (!ssoConfig) throw new BadRequestError({ message: "Failed to find organization SSO data" }); + + // when dto is type id means it's internally used + if (dto.type === "org") { + const { permission } = await permissionService.getOrgPermission( + dto.actor, + dto.actorId, + ssoConfig!.orgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Sso + ); + } + const { + entryPointTag, + entryPointIV, + encryptedEntryPoint, + certTag, + certIV, + encryptedCert, + issuerTag, + issuerIV, + encryptedIssuer + } = ssoConfig; + + const orgBot = await orgBotDal.findOne({ orgId: ssoConfig.orgId }); + if (!orgBot) + throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + let entryPoint = ""; + if (encryptedEntryPoint && entryPointIV && entryPointTag) { + entryPoint = decryptSymmetric({ + ciphertext: encryptedEntryPoint, + key, + tag: entryPointTag, + iv: entryPointIV + }); + } + + let issuer = ""; + if (encryptedIssuer && issuerTag && issuerIV) { + issuer = decryptSymmetric({ + key, + tag: issuerTag, + iv: issuerIV, + ciphertext: encryptedIssuer + }); + } + + let cert = ""; + if (encryptedCert && certTag && certIV) { + cert = decryptSymmetric({ key, tag: certTag, iv: certIV, ciphertext: encryptedCert }); + } + + return { + id: ssoConfig.id, + organization: ssoConfig.orgId, + orgId: ssoConfig.orgId, + authProvider: ssoConfig.authProvider, + isActive: ssoConfig.isActive, + entryPoint, + issuer, + cert + }; + }; + + const samlLogin = async ({ + firstName, + email, + lastName, + authProvider, + orgId, + relayState, + isSignupAllowed + }: TSamlLoginDTO) => { + const appCfg = getConfig(); + let user = await userDal.findUserByEmail(email); + const isSamlSignUpDisabled = !isSignupAllowed && !user; + if (isSamlSignUpDisabled) + throw new BadRequestError({ message: "User signup disabled", name: "Saml SSO login" }); + + const organization = await orgDal.findOrgById(orgId); + if (!organization) throw new BadRequestError({ message: "Org not found" }); + + if (user) { + const hasSamlEnabled = (user.authMethods || []).some((method) => + Object.values(SamlProviders).includes(method as SamlProviders) + ); + await userDal.transaction(async (tx) => { + if (!hasSamlEnabled) { + await userDal.updateById(user.id, { authMethods: [authProvider] }, tx); + } + const [orgMembership] = await orgDal.findMembership({ userId: user.id, orgId }, { tx }); + if (!orgMembership) { + await orgDal.createMembership( + { + userId: user.id, + orgId, + inviteEmail: email, + role: OrgMembershipRole.Member, + status: OrgMembershipStatus.Accepted + }, + tx + ); + } else if (orgMembership.status === OrgMembershipStatus.Invited) { + await orgDal.updateMembershipById( + orgMembership.id, + { + status: OrgMembershipStatus.Accepted + }, + tx + ); + } + }); + } else { + user = await userDal.transaction(async (tx) => { + const newUser = await userDal.create( + { + email, + firstName, + lastName, + authMethods: [authProvider] + }, + tx + ); + await orgDal.createMembership({ + inviteEmail: email, + orgId, + role: OrgMembershipRole.Member, + status: OrgMembershipStatus.Invited + }); + return newUser; + }); + } + const isUserCompleted = Boolean(user.isAccepted); + const providerAuthToken = jwt.sign( + { + authTokenType: AuthTokenType.PROVIDER_TOKEN, + userId: user.id, + email: user.email, + firstName, + lastName, + organizationName: organization.name, + organizationId: organization.id, + authMethod: authProvider, + isUserCompleted, + ...(relayState + ? { + callbackPort: JSON.parse(relayState).callbackPort as string + } + : {}) + }, + appCfg.JWT_AUTH_SECRET, + { + expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME + } + ); + return { isUserCompleted, providerAuthToken }; + }; + + return { + createSamlCfg, + updateSamlCfg, + getSaml, + samlLogin + }; +}; diff --git a/backend-pg/src/ee/services/saml-config/saml-config-types.ts b/backend-pg/src/ee/services/saml-config/saml-config-types.ts new file mode 100644 index 000000000..18a511af5 --- /dev/null +++ b/backend-pg/src/ee/services/saml-config/saml-config-types.ts @@ -0,0 +1,43 @@ +import { TOrgPermission } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; + +export enum SamlProviders { + OKTA_SAML = "okta-saml", + AZURE_SAML = "azure-saml", + JUMPCLOUD_SAML = "jumpcloud-saml" +} + +export type TCreateSamlCfgDTO = { + authProvider: SamlProviders; + isActive: boolean; + entryPoint: string; + issuer: string; + cert: string; +} & TOrgPermission; + +export type TUpdateSamlCfgDTO = Partial<{ + authProvider: SamlProviders; + isActive: boolean; + entryPoint: string; + issuer: string; + cert: string; +}> & + TOrgPermission; + +export type TGetSamlCfgDTO = + | { type: "org"; orgId: string; actor: ActorType; actorId: string } + | { + type: "ssoId"; + id: string; + }; + +export type TSamlLoginDTO = { + email: string; + firstName: string; + lastName?: string; + authProvider: string; + orgId: string; + isSignupAllowed: boolean; + // saml thingy + relayState?: string; +}; 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 b5723bd60..27148c46b 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 @@ -84,12 +84,12 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { childrenMapper: [ { key: "reviewerMemberId", - label: "reviewers", + label: "reviewers" as const, mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => member ? { member, status } : undefined }, - { key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId } - ] as const + { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + ] }); if (!formatedDoc?.[0]) return; return { @@ -213,12 +213,12 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => { childrenMapper: [ { key: "reviewerMemberId", - label: "reviewers", + label: "reviewers" as const, mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => member ? { member, status: s } : undefined }, - { key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId } - ] as const + { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + ] }); return formatedDoc.map((el) => ({ ...el, 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 7bd42f80b..b6627f888 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 @@ -181,7 +181,7 @@ export const secretApprovalRequestServiceFactory = ({ status, statusChangeBy: membership.id }); - return updatedRequest; + return { ...secretApprovalRequest, ...updatedRequest }; }; const mergeSecretApprovalRequest = async ({ 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 index 5f59fdbfd..fa5b87a57 100644 --- a/backend-pg/src/ee/services/secret-rotation/secret-rotation-dal.ts +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-dal.ts @@ -61,7 +61,7 @@ export const secretRotationDalFactory = (db: TDbClient) => { childrenMapper: [ { key: "secId", - label: "outputs", + label: "outputs" as const, mapper: ({ secId, outputKey, @@ -92,7 +92,7 @@ export const secretRotationDalFactory = (db: TDbClient) => { } }) } - ] as const + ] }); } catch (error) { throw new DatabaseError({ error, name: "SecretRotationFind" }); diff --git a/backend-pg/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend-pg/src/ee/services/secret-snapshot/snapshot-dal.ts index c44a37dd3..1d79a2fd1 100644 --- a/backend-pg/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend-pg/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -7,7 +7,8 @@ import { TSecretFolderVersions, TSecretSnapshotFolders, TSecretSnapshots, - TSecretVersions} from "@app/db/schemas"; + TSecretVersions +} from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; @@ -109,15 +110,15 @@ export const snapshotDalFactory = (db: TDbClient) => { childrenMapper: [ { key: "id", - label: "secretVersions", + label: "secretVersions" as const, mapper: (el) => SecretVersionsSchema.parse(el) }, { key: "folderVerId", - label: "folderVersion", + label: "folderVersion" as const, mapper: ({ folderVerId: id, folderVerName: name }) => ({ id, name }) } - ] as const + ] })?.[0]; } catch (error) { throw new DatabaseError({ error, name: "FindSecretSnapshotDataById" }); @@ -237,7 +238,7 @@ export const snapshotDalFactory = (db: TDbClient) => { childrenMapper: [ { key: "id", - label: "secretVersions", + label: "secretVersions" as const, mapper: (el) => ({ ...SecretVersionsSchema.parse(el), latestSecretVersion: el.latestSecretVersion @@ -245,14 +246,14 @@ export const snapshotDalFactory = (db: TDbClient) => { }, { key: "folderVerId", - label: "folderVersion", + label: "folderVersion" as const, mapper: ({ folderVerId: id, folderVerName: name, latestFolderVersion }) => ({ id, name, latestFolderVersion }) } - ] as const + ] }); return formated; } catch (error) { diff --git a/backend-pg/src/folder.ts b/backend-pg/src/folder.ts deleted file mode 100644 index cc4e0bfab..000000000 --- a/backend-pg/src/folder.ts +++ /dev/null @@ -1,190 +0,0 @@ -import dotenv from "dotenv"; - -import { initDbConnection } from "./db"; -import { - SecretVersionsSchema, - TableName, - TSecretFolderVersions, - TSecrets, - TSecretSnapshotFolders, - TSecretSnapshots, - TSecretVersions} from "./db/schemas"; -import { selectAllTableCols, sqlNestRelationships } from "./lib/knex"; - -dotenv.config(); -// 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 db = initDbConnection(process.env.DB_CONNECTION_URI); - - const folders = db - .withRecursive("parent", (qb) => { - qb.from(TableName.Snapshot) - .leftJoin( - TableName.SnapshotFolder, - `${TableName.SnapshotFolder}.snapshotId`, - `${TableName.Snapshot}.id` - ) - .leftJoin( - TableName.SecretFolderVersion, - `${TableName.SnapshotFolder}.folderVersionId`, - `${TableName.SecretFolderVersion}.id` - ) - .select(selectAllTableCols(TableName.Snapshot)) - .select({ depth: 1 }) - .select( - db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"), - db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId") - ) - .where(`${TableName.Snapshot}.id`, "abe694d3-957f-40f5-a907-952d3e5ccaf1") - .union((cb) => - cb - .select(selectAllTableCols(TableName.Snapshot)) - .select({ depth: db.raw("parent.depth + 1") }) - .select( - db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"), - db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId") - ) - .from(TableName.Snapshot) - .join( - db(TableName.Snapshot) - .groupBy("folderId") - .max("createdAt") - .select("folderId") - .as("latestVersion"), - `${TableName.Snapshot}.createdAt`, - "latestVersion.max" - ) - .leftJoin( - TableName.SnapshotFolder, - `${TableName.SnapshotFolder}.snapshotId`, - `${TableName.Snapshot}.id` - ) - .leftJoin( - TableName.SecretFolderVersion, - `${TableName.SnapshotFolder}.folderVersionId`, - `${TableName.SecretFolderVersion}.id` - ) - .join("parent", "parent.folderVerId", `${TableName.Snapshot}.folderId`) - ); - }) - .orderBy("depth", "asc") - .from("parent") - .leftJoin( - TableName.SnapshotSecret, - `parent.id`, - `${TableName.SnapshotSecret}.snapshotId` - ) - .leftJoin( - TableName.SecretVersion, - `${TableName.SnapshotSecret}.secretVersionId`, - `${TableName.SecretVersion}.id` - ) - .leftJoin<{ latestSecretVersion: number }>( - db(TableName.SecretVersion) - .groupBy("secretId") - .select("secretId") - .max("version") - .as("secGroupByMaxVersion"), - `${TableName.SecretVersion}.secretId`, - "secGroupByMaxVersion.secretId" - ) - .leftJoin<{ latestFolderVersion: number }>( - db(TableName.SecretFolderVersion) - .groupBy("folderId") - .select("folderId") - .max("version") - .as("folderGroupByMaxVersion"), - `parent.folderId`, - "folderGroupByMaxVersion.folderId" - ) - .select(selectAllTableCols(TableName.SecretVersion)) - .select( - db.ref("id").withSchema("parent").as("snapshotId"), - db.ref("folderId").withSchema("parent").as("snapshotFolderId"), - db.ref("parentFolderId").withSchema("parent").as("snapshotParentFolderId"), - db.ref("folderVerName").withSchema("parent"), - db.ref("folderVerId").withSchema("parent"), - db.ref("max").withSchema("secGroupByMaxVersion").as("latestSecretVersion"), - db.ref("max").withSchema("folderGroupByMaxVersion").as("latestFolderVersion") - ); - console.log(folders.toSQL()); - const data = await folders; - // console.log(data); - const formated = sqlNestRelationships({ - data, - key: "snapshotId", - parentMapper: ({ snapshotId: id }) => ({ - id - }), - childrenMapper: [ - { - key: "id", - label: "secretVersions", - mapper: (el) => ({ - ...SecretVersionsSchema.parse(el), - latestSecretVersion: el.latestSecretVersion - }) - }, - { - key: "folderVerId", - label: "folderVersion", - mapper: ({ folderVerId: id, folderVerName: name, latestFolderVersion }) => ({ - id, - name, - version: latestFolderVersion - }) - } - ] as const - }); - console.log(formated); - process.exit(0); -}; -main(); diff --git a/backend-pg/src/lib/crypto/encryption.ts b/backend-pg/src/lib/crypto/encryption.ts index 18b6b7312..938db632c 100644 --- a/backend-pg/src/lib/crypto/encryption.ts +++ b/backend-pg/src/lib/crypto/encryption.ts @@ -122,6 +122,8 @@ export const decryptAsymmetric = ({ return naclUtils.encodeUTF8(plaintext); }; +export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64"); + export const generateAsymmetricKeyPair = () => { const pair = nacl.box.keyPair(); diff --git a/backend-pg/src/lib/fn/object.ts b/backend-pg/src/lib/fn/object.ts index 34784766e..2fadc685c 100644 --- a/backend-pg/src/lib/fn/object.ts +++ b/backend-pg/src/lib/fn/object.ts @@ -15,3 +15,23 @@ export const pick = ( {} as Pick ); }; + +/** + * Removes (shakes out) undefined entries from an + * object. Optional second argument shakes out values + * by custom evaluation. + */ +export const shake = ( + obj: T, + filter: (value: any) => boolean = (x) => x === undefined || x === null +): Omit => { + if (!obj) return {} as T; + const keys = Object.keys(obj) as (keyof T)[]; + return keys.reduce((acc, key) => { + if (filter(obj[key])) { + return acc; + } + acc[key] = obj[key]; + return acc; + }, {} as T); +}; diff --git a/backend-pg/src/lib/knex/select.ts b/backend-pg/src/lib/knex/select.ts index c183907b6..d7dfa77f0 100644 --- a/backend-pg/src/lib/knex/select.ts +++ b/backend-pg/src/lib/knex/select.ts @@ -3,12 +3,12 @@ import { Tables } from "knex/types/tables"; export const selectAllTableCols = (tableName: Tname) => `${tableName}.*` as keyof Tables[Tname]["base"]; -export const stripUndefinedInWhere = (val: T) => { +export const stripUndefinedInWhere = (val: T): Exclude => { const copy = val; Object.entries(copy).forEach(([key, value]) => { if (typeof value === "undefined") { delete copy[key as keyof T]; } }); - return copy; + return copy as Exclude; }; diff --git a/backend-pg/src/queue/queue-service.ts b/backend-pg/src/queue/queue-service.ts index c1042a9a4..0023b02de 100644 --- a/backend-pg/src/queue/queue-service.ts +++ b/backend-pg/src/queue/queue-service.ts @@ -1,12 +1,16 @@ import { Job, JobsOptions, Queue, Worker, WorkerListener } from "bullmq"; import Redis from "ioredis"; +import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; + export enum QueueName { - SecretRotation = "secret-rotation" + SecretRotation = "secret-rotation", + AuditLog = "audit-log" } export enum QueueJobs { - SecretRotation = "secret-rotation-job" + SecretRotation = "secret-rotation-job", + AuditLog = "audit-log-job" } export type TQueueJobTypes = { @@ -14,6 +18,10 @@ export type TQueueJobTypes = { payload: { rotationId: string }; name: QueueJobs.SecretRotation; }; + [QueueName.AuditLog]: { + name: QueueJobs.AuditLog; + payload: TCreateAuditLogDTO; + }; }; export type TQueueServiceFactory = ReturnType; @@ -65,7 +73,7 @@ export const queueServiceFactory = (redisUrl: string) => { name: T, job: TQueueJobTypes[T]["name"], data: TQueueJobTypes[T]["payload"], - opts: JobsOptions & { jobId: string } + opts: JobsOptions & { jobId?: string } ) => { const q = queueContainer[name]; await q.add(job, data, opts); diff --git a/backend-pg/src/server/plugins/audit-log.ts b/backend-pg/src/server/plugins/audit-log.ts new file mode 100644 index 000000000..1456e5376 --- /dev/null +++ b/backend-pg/src/server/plugins/audit-log.ts @@ -0,0 +1,55 @@ +import fp from "fastify-plugin"; + +import { UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; +import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; + +export const getUserAgentType = (userAgent: string | undefined) => { + if (userAgent === undefined) { + return UserAgentType.OTHER; + } + if (userAgent === UserAgentType.CLI) { + return UserAgentType.CLI; + } + if (userAgent === UserAgentType.K8_OPERATOR) { + return UserAgentType.K8_OPERATOR; + } + if (userAgent === UserAgentType.TERRAFORM) { + return UserAgentType.TERRAFORM; + } + if (userAgent.toLowerCase().includes("mozilla")) { + return UserAgentType.WEB; + } + if (userAgent.includes(UserAgentType.NODE_SDK)) { + return UserAgentType.NODE_SDK; + } + if (userAgent.includes(UserAgentType.PYTHON_SDK)) { + return UserAgentType.PYTHON_SDK; + } + return UserAgentType.OTHER; +}; + +export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => { + server.decorateRequest("auditLogInfo", null); + server.addHook("onRequest", async (req) => { + if (!req.auth) return; + const userAgent = req.headers["user-agent"] ?? ""; + const payload = { + ipAddress: req.realIp, + userAgent, + userAgentType: getUserAgentType(userAgent) + } as typeof req.auditLogInfo; + if (req.auth.actor === ActorType.USER) { + payload.actor = { + type: ActorType.USER, + metadata: { + email: req.auth.user.email, + userId: req.auth.userId + } + }; + } else { + throw new BadRequestError({ message: "Missing logic for other actor" }); + } + req.auditLogInfo = payload; + }); +}); diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts index 0816d99fa..e6bac2856 100644 --- a/backend-pg/src/server/routes/index.ts +++ b/backend-pg/src/server/routes/index.ts @@ -2,8 +2,13 @@ import { Knex } from "knex"; import { z } from "zod"; import { registerV1EERoutes } from "@app/ee/routes/v1"; +import { auditLogDalFactory } from "@app/ee/services/audit-log/audit-log-dal"; +import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; +import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { permissionDalFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { samlConfigDalFactory } from "@app/ee/services/saml-config/saml-config-dal"; +import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-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"; @@ -43,6 +48,7 @@ import { integrationServiceFactory } from "@app/services/integration/integration import { integrationAuthDalFactory } from "@app/services/integration-auth/integration-auth-dal"; import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; import { incidentContactDalFactory } from "@app/services/org/incident-contacts-dal"; +import { orgBotDalFactory } from "@app/services/org/org-bot-dal"; import { orgDalFactory } from "@app/services/org/org-dal"; import { orgRoleDalFactory } from "@app/services/org/org-role-dal"; import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; @@ -80,6 +86,7 @@ import { userServiceFactory } from "@app/services/user/user-service"; import { webhookDalFactory } from "@app/services/webhook/webhook-dal"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; +import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectPermission } from "../plugins/auth/inject-permission"; import { registerV1Routes } from "./v1"; @@ -99,6 +106,7 @@ export const registerRoutes = async ( const authDal = authDalFactory(db); const authTokenDal = tokenDalFactory(db); const orgDal = orgDalFactory(db); + const orgBotDal = orgBotDalFactory(db); const incidentContactDal = incidentContactDalFactory(db); const orgRoleDal = orgRoleDalFactory(db); const superAdminDal = superAdminDalFactory(db); @@ -132,8 +140,11 @@ export const registerRoutes = async ( const identityUaDal = identityUaDalFactory(db); const identityUaClientSecretDal = identityUaClientSecretDalFactory(db); + const auditLogDal = auditLogDalFactory(db); + // ee db layer ops const permissionDal = permissionDalFactory(db); + const samlConfigDal = samlConfigDalFactory(db); const sapApproverDal = sapApproverDalFactory(db); const secretApprovalPolicyDal = secretApprovalPolicyDalFactory(db); const secretApprovalRequestDal = secretApprovalRequestDalFactory(db); @@ -146,6 +157,8 @@ export const registerRoutes = async ( const snapshotFolderDal = snapshotFolderDalFactory(db); const permissionService = permissionServiceFactory({ permissionDal, orgRoleDal, projectRoleDal }); + const auditLogQueue = auditLogQueueServiceFactory({ auditLogDal, queueService }); + const auditLogService = auditLogServiceFactory({ auditLogDal, permissionService, auditLogQueue }); const sapService = secretApprovalPolicyServiceFactory({ projectMembershipDal, projectEnvDal, @@ -153,6 +166,13 @@ export const registerRoutes = async ( permissionService, secretApprovalPolicyDal }); + const samlService = samlConfigServiceFactory({ + permissionService, + orgBotDal, + orgDal, + userDal, + samlConfigDal + }); const sarService = secretApprovalRequestServiceFactory({ permissionService, folderDal, @@ -180,7 +200,8 @@ export const registerRoutes = async ( incidentContactDal, tokenService, smtpService, - userDal + userDal, + orgBotDal }); const signupService = authSignupServiceFactory({ tokenService, @@ -354,7 +375,9 @@ export const registerRoutes = async ( secretApprovalPolicy: sapService, secretApprovalRequest: sarService, secretRotation: secretRotationService, - snapshot: snapshotService + snapshot: snapshotService, + saml: samlService, + auditLog: auditLogService }); server.decorate("store", { @@ -363,6 +386,7 @@ export const registerRoutes = async ( await server.register(injectIdentity); await server.register(injectPermission); + await server.register(injectAuditLogInfo); server.route({ url: "/status", @@ -394,9 +418,13 @@ export const registerRoutes = async ( }); // register routes for v1 - await server.register(registerV1Routes, { prefix: "/v1" }); + await server.register( + async (v1Server) => { + await v1Server.register(registerV1EERoutes); + await v1Server.register(registerV1Routes); + }, + { prefix: "/v1" } + ); await server.register(registerV2Routes, { prefix: "/v2" }); await server.register(registerV3Routes, { prefix: "/v3" }); - - await server.register(registerV1EERoutes, { prefix: "/ee/v1" }); }; diff --git a/backend-pg/src/server/routes/v1/identity-router.ts b/backend-pg/src/server/routes/v1/identity-router.ts index 998ed9b43..f86cc9528 100644 --- a/backend-pg/src/server/routes/v1/identity-router.ts +++ b/backend-pg/src/server/routes/v1/identity-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -28,6 +29,19 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { ...req.body, orgId: req.body.organizationId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.body.organizationId, + event: { + type: EventType.CREATE_IDENTITY, + metadata: { + name: identity.name, + identityId: identity.id + } + } + }); + return { identity }; } }); @@ -57,6 +71,19 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { id: req.params.identityId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identity.orgId, + event: { + type: EventType.UPDATE_IDENTITY, + metadata: { + name: identity.name, + identityId: identity.id + } + } + }); + return { identity }; } }); @@ -81,6 +108,17 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, id: req.params.identityId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identity.orgId, + event: { + type: EventType.DELETE_IDENTITY, + metadata: { + identityId: identity.id + } + } + }); return { identity }; } }); diff --git a/backend-pg/src/server/routes/v1/identity-ua.ts b/backend-pg/src/server/routes/v1/identity-ua.ts index ed7ab3f67..662dd071a 100644 --- a/backend-pg/src/server/routes/v1/identity-ua.ts +++ b/backend-pg/src/server/routes/v1/identity-ua.ts @@ -1,8 +1,10 @@ import { z } from "zod"; import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({ id: true, @@ -36,10 +38,21 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { identityUa, accessToken } = await server.services.identityUa.login( - req.body.clientId, - req.body.clientSecret - ); + const { identityUa, accessToken, identityAccessToken, validClientSecretInfo } = + await server.services.identityUa.login(req.body.clientId, req.body.clientSecret); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + event: { + type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, + metadata: { + clientSecretId: validClientSecretInfo.id, + identityId: identityUa.identityId, + identityAccessTokenId: identityAccessToken.id, + identityUniversalAuthId: identityUa.id + } + } + }); return { accessToken, tokenType: "Bearer" as const, @@ -102,6 +115,24 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { ...req.body, identityId: req.params.identityId }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityUniversalAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityUniversalAuth.identityId, + accessTokenTTL: identityUniversalAuth.accessTokenTTL, + accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL, + accessTokenTrustedIps: + identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + clientSecretTrustedIps: + identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit + } + } + }); + return { identityUniversalAuth }; } }); @@ -152,6 +183,25 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { ...req.body, identityId: req.params.identityId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityUniversalAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityUniversalAuth.identityId, + accessTokenTTL: identityUniversalAuth.accessTokenTTL, + accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL, + accessTokenTrustedIps: + identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + clientSecretTrustedIps: + identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit + } + } + }); + return { identityUniversalAuth }; } }); @@ -176,6 +226,18 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, identityId: req.params.identityId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityUniversalAuth.orgId, + event: { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityUniversalAuth.identityId + } + } + }); + return { identityUniversalAuth }; } }); @@ -201,13 +263,26 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { clientSecret, clientSecretData } = + const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({ actor: req.permission.type, actorId: req.permission.id, identityId: req.params.identityId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId, + event: { + type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: req.params.identityId, + clientSecretId: clientSecretData.id + } + } + }); + return { clientSecret, clientSecretData }; } }); @@ -227,10 +302,22 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const clientSecretData = await server.services.identityUa.getUaClientSecrets({ - actor: req.permission.type, - actorId: req.permission.id, - identityId: req.params.identityId + const { clientSecrets: clientSecretData, orgId } = + await server.services.identityUa.getUaClientSecrets({ + actor: req.permission.type, + actorId: req.permission.id, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId, + event: { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS, + metadata: { + identityId: req.params.identityId + } + } }); return { clientSecretData }; } @@ -258,6 +345,19 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { identityId: req.params.identityId, clientSecretId: req.params.clientSecretId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: clientSecretData.orgId, + event: { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: clientSecretData.identityId, + clientSecretId: clientSecretData.id + } + } + }); + return { clientSecretData }; } }); diff --git a/backend-pg/src/server/routes/v1/integration-auth-router.ts b/backend-pg/src/server/routes/v1/integration-auth-router.ts index ed8397946..188d07c66 100644 --- a/backend-pg/src/server/routes/v1/integration-auth-router.ts +++ b/backend-pg/src/server/routes/v1/integration-auth-router.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -77,6 +78,18 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) actor: req.permission.type, id: req.params.integrationAuthId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: integrationAuth.projectId, + event: { + type: EventType.UNAUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + } + }); + return { integrationAuth }; } }); @@ -85,6 +98,45 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) url: "/oauth-token", method: "POST", onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z.object({ + workspaceId: z.string().trim(), + code: z.string().trim(), + integration: z.string().trim(), + url: z.string().trim().url().optional() + }), + response: { + 200: z.object({ + integrationAuth: integrationAuthPubSchema + }) + } + }, + handler: async (req) => { + const integrationAuth = await server.services.integrationAuth.oauthExchange({ + actorId: req.permission.id, + actor: req.permission.type, + projectId: req.body.workspaceId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.AUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + } + }); + return { integrationAuth }; + } + }); + + server.route({ + url: "/access-token", + method: "POST", + onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ workspaceId: z.string().trim(), @@ -108,6 +160,17 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) projectId: req.body.workspaceId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.AUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + } + }); return { integrationAuth }; } }); diff --git a/backend-pg/src/server/routes/v1/integration-router.ts b/backend-pg/src/server/routes/v1/integration-router.ts index 5d2c14d59..23d8ef8be 100644 --- a/backend-pg/src/server/routes/v1/integration-router.ts +++ b/backend-pg/src/server/routes/v1/integration-router.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { IntegrationsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { shake } from "@app/lib/fn"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -45,11 +47,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const integration = await server.services.integration.createIntegration({ + const { integration, integrationAuth } = await server.services.integration.createIntegration({ actorId: req.permission.id, actor: req.permission.type, ...req.body }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: integrationAuth.projectId, + event: { + type: EventType.CREATE_INTEGRATION, + metadata: shake({ + integrationId: integration.id.toString(), + integration: integration.integration, + environment: req.body.sourceEnvironment, + secretPath: req.body.secretPath, + url: integration.url, + app: integration.app, + appId: integration.appId, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId, + targetService: integration.targetService, + targetServiceId: integration.targetServiceId, + path: integration.path, + region: integration.region + }) as any + } + }); return { integration }; } }); @@ -108,6 +132,29 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, id: req.params.integrationId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: integration.projectId, + event: { + type: EventType.DELETE_INTEGRATION, + metadata: shake({ + integrationId: integration.id, + integration: integration.integration, + environment: integration.environment.slug, + secretPath: integration.secretPath, + url: integration.url, + app: integration.app, + appId: integration.appId, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId, + targetService: integration.targetService, + targetServiceId: integration.targetServiceId, + path: integration.path, + region: integration.region + }) as any + } + }); return { integration }; } }); diff --git a/backend-pg/src/server/routes/v1/project-env-router.ts b/backend-pg/src/server/routes/v1/project-env-router.ts index 0c413b537..186c7f389 100644 --- a/backend-pg/src/server/routes/v1/project-env-router.ts +++ b/backend-pg/src/server/routes/v1/project-env-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { ProjectEnvironmentsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -32,6 +33,18 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.CREATE_ENVIRONMENT, + metadata: { + name: environment.name, + slug: environment.slug + } + } + }); return { message: "Successfully created new environment", workspace: req.params.workspaceId, @@ -62,13 +75,28 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { - const environment = await server.services.projectEnv.updateEnvironment({ + const { environment, old } = await server.services.projectEnv.updateEnvironment({ actorId: req.permission.id, actor: req.permission.type, projectId: req.params.workspaceId, id: req.params.id, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.UPDATE_ENVIRONMENT, + metadata: { + oldName: old.name, + oldSlug: old.slug, + newName: old.name, + newSlug: old.slug + } + } + }); + return { message: "Successfully updated environment", workspace: req.params.workspaceId, @@ -101,6 +129,19 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, id: req.params.id }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: environment.projectId, + event: { + type: EventType.DELETE_ENVIRONMENT, + metadata: { + slug: environment.slug, + name: environment.name + } + } + }); + return { message: "Successfully deleted environment", workspace: req.params.workspaceId, diff --git a/backend-pg/src/server/routes/v1/project-membership-router.ts b/backend-pg/src/server/routes/v1/project-membership-router.ts index c78281e69..99d680c2c 100644 --- a/backend-pg/src/server/routes/v1/project-membership-router.ts +++ b/backend-pg/src/server/routes/v1/project-membership-router.ts @@ -1,6 +1,12 @@ import { z } from "zod"; -import { ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { + OrgMembershipsSchema, + ProjectMembershipsSchema, + UserEncryptionKeysSchema, + UsersSchema +} from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -42,6 +48,55 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } }); + server.route({ + url: "/:workspaceId/memberships", + method: "POST", + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + members: z + .object({ + orgMembershipId: z.string().trim(), + workspaceEncryptedKey: z.string().trim(), + workspaceEncryptedNonce: z.string().trim() + }) + .array() + .min(1) + }), + response: { + 200: z.object({ + success: z.boolean(), + data: OrgMembershipsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const data = await server.services.projectMembership.addUsersToProject({ + actorId: req.permission.id, + actor: req.permission.type, + projectId: req.params.workspaceId, + members: req.body.members + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.params.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.ADD_BATCH_WORKSPACE_MEMBER, + metadata: data.map(({ userId }) => ({ + userId: userId || "", + email: "" + })) + } + }); + + return { data, success: true }; + } + }); + server.route({ url: "/:workspaceId/memberships/:membershipId", method: "PATCH", @@ -68,6 +123,20 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider membershipId: req.params.membershipId, role: req.body.role }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_USER_WORKSPACE_ROLE, + metadata: { + userId: membership.userId, + newRole: req.body.role, + oldRole: membership.role, + email: "" + } + } + }); return { membership }; } }); @@ -94,6 +163,18 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider projectId: req.params.workspaceId, membershipId: req.params.membershipId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.REMOVE_WORKSPACE_MEMBER, + metadata: { + userId: membership.userId, + email: "" + } + } + }); return { membership }; } }); diff --git a/backend-pg/src/server/routes/v1/project-router.ts b/backend-pg/src/server/routes/v1/project-router.ts index 08d30ecfe..237a7b1d3 100644 --- a/backend-pg/src/server/routes/v1/project-router.ts +++ b/backend-pg/src/server/routes/v1/project-router.ts @@ -8,6 +8,7 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -265,6 +266,18 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, email: req.body.email }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.ADD_WORKSPACE_MEMBER, + metadata: { + userId: invitee.id, + email: invitee.email + } + } + }); return { invitee, latestKey }; } }); diff --git a/backend-pg/src/server/routes/v1/secret-folder-router.ts b/backend-pg/src/server/routes/v1/secret-folder-router.ts index bc3b64963..d267b8d65 100644 --- a/backend-pg/src/server/routes/v1/secret-folder-router.ts +++ b/backend-pg/src/server/routes/v1/secret-folder-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretFoldersSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -28,6 +29,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => actor: req.permission.type, ...req.body }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.projectId, + event: { + type: EventType.CREATE_FOLDER, + metadata: { + environment: req.body.environment, + folderId: folder.id, + folderName: folder.name, + folderPath: req.body.path + } + } + }); return { folder }; } }); @@ -53,12 +67,26 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { - const folder = await server.services.folder.updateFolder({ + const { folder, old } = await server.services.folder.updateFolder({ actorId: req.permission.id, actor: req.permission.type, ...req.body, id: req.params.folderId }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.projectId, + event: { + type: EventType.UPDATE_FOLDER, + metadata: { + environment: req.body.environment, + folderId: folder.id, + folderPath: req.body.path, + newFolderName: folder.name, + oldFolderName: old.name + } + } + }); return { folder }; } }); @@ -89,6 +117,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => ...req.body, id: req.params.folderId }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.projectId, + event: { + type: EventType.DELETE_FOLDER, + metadata: { + environment: req.body.environment, + folderId: folder.id, + folderPath: req.body.path, + folderName: folder.name + } + } + }); return { folder }; } }); diff --git a/backend-pg/src/server/routes/v1/secret-import-router.ts b/backend-pg/src/server/routes/v1/secret-import-router.ts index 90fe43434..d816251ee 100644 --- a/backend-pg/src/server/routes/v1/secret-import-router.ts +++ b/backend-pg/src/server/routes/v1/secret-import-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -37,6 +38,22 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => ...req.body, data: req.body.import }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.projectId, + event: { + type: EventType.CREATE_SECRET_IMPORT, + metadata: { + secretImportId: secretImport.id, + folderId: secretImport.folderId, + importFromSecretPath: secretImport.importPath, + importFromEnvironment: secretImport.importEnv.slug, + importToEnvironment: req.body.environment, + importToSecretPath: req.body.path + } + } + }); return { message: "Successfully created secret import", secretImport }; } }); @@ -78,6 +95,22 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => ...req.body, data: req.body.import }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.projectId, + event: { + type: EventType.UPDATE_SECRET_IMPORT, + metadata: { + secretImportId: secretImport.id, + folderId: secretImport.folderId, + position: secretImport.position, + importToEnvironment: req.body.environment, + importToSecretPath: req.body.path + } + } + }); + return { message: "Successfully updated secret import", secretImport }; } }); @@ -113,6 +146,22 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => id: req.params.secretImportId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.projectId, + event: { + type: EventType.DELETE_SECRET_IMPORT, + metadata: { + secretImportId: secretImport.id, + folderId: secretImport.folderId, + importFromEnvironment: secretImport.importEnv.slug, + importFromSecretPath: secretImport.importPath, + importToEnvironment: req.body.environment, + importToSecretPath: req.body.path + } + } + }); return { message: "Successfully deleted secret import", secretImport }; } }); @@ -146,6 +195,19 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => actor: req.permission.type, ...req.query }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_SECRET_IMPORTS, + metadata: { + environment: req.query.environment, + folderId: secretImports?.[0]?.folderId, + numberOfImports: secretImports.length + } + } + }); return { message: "Successfully fetched secret imports", secretImports }; } }); diff --git a/backend-pg/src/server/routes/v1/webhook-router.ts b/backend-pg/src/server/routes/v1/webhook-router.ts index f1146aa42..89564d9a3 100644 --- a/backend-pg/src/server/routes/v1/webhook-router.ts +++ b/backend-pg/src/server/routes/v1/webhook-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { WebhooksSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -9,10 +10,10 @@ export const sanitizedWebhookSchema = WebhooksSchema.omit({ iv: true, tag: true, algorithm: true, - keyEncoding: true, + keyEncoding: true }).merge( z.object({ - projectId:z.string(), + projectId: z.string(), environment: z.object({ id: z.string(), name: z.string(), @@ -48,6 +49,22 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { projectId: req.body.workspaceId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.body.workspaceId, + event: { + type: EventType.CREATE_WEBHOOK, + metadata: { + environment: webhook.environment.slug, + webhookId: webhook.id, + isDisabled: webhook.isDisabled, + secretPath: webhook.secretPath, + webhookUrl: webhook.url + } + } + }); + return { message: "Successfully created webhook", webhook }; } }); @@ -77,6 +94,22 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { id: req.params.webhookId, isDisabled: req.body.isDisabled }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: webhook.projectId, + event: { + type: EventType.UPDATE_WEBHOOK_STATUS, + metadata: { + environment: webhook.environment.slug, + webhookId: webhook.id, + isDisabled: webhook.isDisabled, + secretPath: webhook.secretPath, + webhookUrl: webhook.url + } + } + }); + return { message: "Successfully updated webhook", webhook }; } }); @@ -96,6 +129,22 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, id: req.params.webhookId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: webhook.projectId, + event: { + type: EventType.DELETE_WEBHOOK, + metadata: { + environment: webhook.environment.slug, + webhookId: webhook.id, + isDisabled: webhook.isDisabled, + secretPath: webhook.secretPath, + webhookUrl: webhook.url + } + } + }); + return { message: "Successfully deleted webhook", webhook }; } }); diff --git a/backend-pg/src/server/routes/v2/project-router.ts b/backend-pg/src/server/routes/v2/project-router.ts index 53becc6f2..86f25d89d 100644 --- a/backend-pg/src/server/routes/v2/project-router.ts +++ b/backend-pg/src/server/routes/v2/project-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { ProjectKeysSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -30,6 +31,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.GET_WORKSPACE_KEY, + metadata: { + keyId: key.id + } + } + }); + return key; } }); diff --git a/backend-pg/src/server/routes/v2/service-token-router.ts b/backend-pg/src/server/routes/v2/service-token-router.ts index 6531e16e0..de4e1c24b 100644 --- a/backend-pg/src/server/routes/v2/service-token-router.ts +++ b/backend-pg/src/server/routes/v2/service-token-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { ServiceTokensSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -68,6 +69,18 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => ...req.body, projectId: req.body.workspaceId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: serviceToken.projectId, + event: { + type: EventType.CREATE_SERVICE_TOKEN, + metadata: { + name: serviceToken.name, + scopes: req.body.scopes + } + } + }); return { serviceToken: token, serviceTokenData: serviceToken }; } }); @@ -92,6 +105,19 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => actor: req.permission.type, id: req.params.serviceTokenId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: serviceTokenData.projectId, + event: { + type: EventType.DELETE_SERVICE_TOKEN, + metadata: { + name: serviceTokenData.name, + scopes: serviceTokenData.scopes as Array<{ environment: string; secretPath: string }> + } + } + }); + return { serviceTokenData }; } }); diff --git a/backend-pg/src/server/routes/v3/secret-router.ts b/backend-pg/src/server/routes/v3/secret-router.ts index 38352f343..298e4c3a8 100644 --- a/backend-pg/src/server/routes/v3/secret-router.ts +++ b/backend-pg/src/server/routes/v3/secret-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretApprovalRequestsSchema, SecretsSchema, SecretType } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -35,6 +36,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { path: req.query.secretPath }); + await server.services.auditLog.createAuditLog({ + projectId: req.query.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRETS, + metadata: { + environment: req.query.environment, + secretPath: req.query.secretPath, + numberOfSecrets: secrets.length + } + } + }); + return { secrets }; } }); @@ -74,6 +88,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { type: req.query.type }); + await server.services.auditLog.createAuditLog({ + projectId: req.query.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRET, + metadata: { + environment: req.query.environment, + secretPath: req.query.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version + } + } + }); return { secret }; } }); @@ -167,6 +195,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ] } }); + + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: approval.committerId, + secretApprovalRequestId: approval.id, + secretApprovalRequestSlug: approval.slug + } + } + }); return { approval }; } } @@ -191,6 +232,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata }); + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version + } + } + }); return { secret }; } }); @@ -296,6 +351,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ] } }); + + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: approval.committerId, + secretApprovalRequestId: approval.id, + secretApprovalRequestSlug: approval.slug + } + } + }); return { approval }; } } @@ -325,6 +393,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { newSecretName }); + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version + } + } + }); return { secret }; } }); @@ -382,6 +464,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ] } }); + + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: approval.committerId, + secretApprovalRequestId: approval.id, + secretApprovalRequestSlug: approval.slug + } + } + }); return { approval }; } } @@ -397,6 +492,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretId }); + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version + } + } + }); return { secret }; } }); @@ -463,6 +572,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { [CommitType.Create]: inputSecrets.filter(({ type }) => type === "shared") } }); + + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: approval.committerId, + secretApprovalRequestId: approval.id, + secretApprovalRequestSlug: approval.slug + } + } + }); return { approval }; } } @@ -476,6 +598,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: inputSecrets }); + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret, i) => ({ + secretId: secret.id, + secretKey: inputSecrets[i].secretName, + secretVersion: secret.version + })) + } + } + }); return { secrets }; } }); @@ -542,6 +680,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { [CommitType.Update]: inputSecrets.filter(({ type }) => type === "shared") } }); + + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: approval.committerId, + secretApprovalRequestId: approval.id, + secretApprovalRequestSlug: approval.slug + } + } + }); return { approval }; } } @@ -554,6 +705,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: inputSecrets }); + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret, i) => ({ + secretId: secret.id, + secretKey: inputSecrets[i].secretName, + secretVersion: secret.version + })) + } + } + }); return { secrets }; } }); @@ -609,6 +776,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { [CommitType.Delete]: inputSecrets.filter(({ type }) => type === "shared") } }); + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: approval.committerId, + secretApprovalRequestId: approval.id, + secretApprovalRequestSlug: approval.slug + } + } + }); return { approval }; } } @@ -621,6 +800,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: inputSecrets }); + await server.services.auditLog.createAuditLog({ + projectId: req.body.workspaceId, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret, i) => ({ + secretId: secret.id, + secretKey: inputSecrets[i].secretName, + secretVersion: secret.version + })) + } + } + }); return { secrets }; } }); diff --git a/backend-pg/src/services/auth/auth-type.ts b/backend-pg/src/services/auth/auth-type.ts index df4ca4523..a23bf911f 100644 --- a/backend-pg/src/services/auth/auth-type.ts +++ b/backend-pg/src/services/auth/auth-type.ts @@ -32,7 +32,8 @@ export enum AuthMode { export enum ActorType { // would extend to AWS, Azure, ... USER = "user", // userIdentity SERVICE = "service", - IDENTITY = "identity" + IDENTITY = "identity", + Machine = "machine" } export type AuthModeJwtTokenPayload = { diff --git a/backend-pg/src/services/identity-ua/identity-ua-service.ts b/backend-pg/src/services/identity-ua/identity-ua-service.ts index f4e6b9944..26427c709 100644 --- a/backend-pg/src/services/identity-ua/identity-ua-service.ts +++ b/backend-pg/src/services/identity-ua/identity-ua-service.ts @@ -132,7 +132,7 @@ export const identityUaServiceFactory = ({ : identityAccessToken.accessTokenMaxTTL } ); - return { accessToken, identityUa }; + return { accessToken, identityUa, validClientSecretInfo, identityAccessToken }; }; const attachUa = async ({ @@ -216,7 +216,7 @@ export const identityUaServiceFactory = ({ ); return doc; }); - return identityUa; + return { ...identityUa, orgId: identityMembershipOrg.orgId }; }; const updateUa = async ({ @@ -295,7 +295,7 @@ export const identityUaServiceFactory = ({ ? JSON.stringify(reformattedAccessTokenTrustedIps) : undefined }); - return updatedUaAuth; + return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId }; }; const getIdentityUa = async ({ identityId, actorId, actor }: TGetUaDTO) => { @@ -317,7 +317,7 @@ export const identityUaServiceFactory = ({ OrgPermissionActions.Read, OrgPermissionSubjects.Identity ); - return uaIdentityAuth; + return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; }; const createUaClientSecret = async ({ @@ -375,7 +375,8 @@ export const identityUaServiceFactory = ({ return { clientSecret, clientSecretData: identityUaClientSecret, - uaAuth: identityUniversalAuth + uaAuth: identityUniversalAuth, + orgId: identityMembershipOrg.orgId }; }; @@ -415,7 +416,7 @@ export const identityUaServiceFactory = ({ identityUAId: identityUniversalAuth.id, isClientSecretRevoked: false }); - return clientSecrets; + return { clientSecrets, orgId: identityMembershipOrg.orgId }; }; const revokeUaClientSecret = async ({ @@ -454,7 +455,7 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDal.updateById(clientSecretId, { isClientSecretRevoked: true }); - return clientSecret; + return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; }; return { diff --git a/backend-pg/src/services/identity/identity-service.ts b/backend-pg/src/services/identity/identity-service.ts index 7ce2d32ec..e13b99faf 100644 --- a/backend-pg/src/services/identity/identity-service.ts +++ b/backend-pg/src/services/identity/identity-service.ts @@ -111,8 +111,7 @@ export const identityServiceFactory = ({ return newIdentity; }); - // TODO(akhilmhdh-pg): add audit log here - return identity; + return { ...identity, orgId: identityOrgMembership.orgId }; }; const deleteIdentity = async ({ actorId, actor, id }: TDeleteIdentityDTO) => { @@ -139,7 +138,7 @@ export const identityServiceFactory = ({ throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); const deletedIdentity = await identityDal.deleteById(id); - return deletedIdentity; + return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; }; const listOrgIdentities = async ({ orgId, actor, actorId }: TOrgPermission) => { diff --git a/backend-pg/src/services/identity/identity-types.ts b/backend-pg/src/services/identity/identity-types.ts index f104fdcda..10b943667 100644 --- a/backend-pg/src/services/identity/identity-types.ts +++ b/backend-pg/src/services/identity/identity-types.ts @@ -1,3 +1,4 @@ +import { IPType } from "@app/lib/ip"; import { TOrgPermission } from "@app/lib/types"; export type TCreateIdentityDTO = { @@ -14,3 +15,9 @@ export type TUpdateIdentityDTO = { export type TDeleteIdentityDTO = { id: string; } & Omit; + +export interface TIdentityTrustedIp { + ipAddress: string; + type: IPType; + prefix: number; +} diff --git a/backend-pg/src/services/integration-auth/integration-auth-service.ts b/backend-pg/src/services/integration-auth/integration-auth-service.ts index b290e964d..e39856be2 100644 --- a/backend-pg/src/services/integration-auth/integration-auth-service.ts +++ b/backend-pg/src/services/integration-auth/integration-auth-service.ts @@ -998,7 +998,7 @@ export const integrationAuthServiceFactory = ({ }; return { -listIntegrationAuthByProjectId, + listIntegrationAuthByProjectId, getIntegrationOptions, getIntegrationAuth, oauthExchange, diff --git a/backend-pg/src/services/integration-auth/integration-auth-types.ts b/backend-pg/src/services/integration-auth/integration-auth-types.ts index d830c9176..bd365dc03 100644 --- a/backend-pg/src/services/integration-auth/integration-auth-types.ts +++ b/backend-pg/src/services/integration-auth/integration-auth-types.ts @@ -7,7 +7,7 @@ export type TGetIntegrationAuthDTO = { export type TOauthExchangeDTO = { integration: string; code: string; - url: string; + url?: string; } & TProjectPermission; export type TSaveIntegrationAccessTokenDTO = { diff --git a/backend-pg/src/services/integration/integration-service.ts b/backend-pg/src/services/integration/integration-service.ts index 44b34f000..ad013ecc0 100644 --- a/backend-pg/src/services/integration/integration-service.ts +++ b/backend-pg/src/services/integration/integration-service.ts @@ -91,7 +91,7 @@ export const integrationServiceFactory = ({ }); // TODO(akhilmhdh-pg): audit log - return integration; + return { integration, integrationAuth }; }; const updateIntegration = async ({ @@ -150,7 +150,7 @@ export const integrationServiceFactory = ({ ); const deletedIntegration = await integrationDal.deleteById(id); - return deletedIntegration; + return { ...integration, ...deletedIntegration }; }; const listIntegrationByProject = async ({ actor, actorId, projectId }: TProjectPermission) => { diff --git a/backend-pg/src/services/org/org-bot-dal.ts b/backend-pg/src/services/org/org-bot-dal.ts new file mode 100644 index 000000000..b4ac0456f --- /dev/null +++ b/backend-pg/src/services/org/org-bot-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TOrgBotDalFactory = ReturnType; + +export const orgBotDalFactory = (db: TDbClient) => { + const orgBotOrm = ormify(db, TableName.OrgBot); + return orgBotOrm; +}; diff --git a/backend-pg/src/services/org/org-dal.ts b/backend-pg/src/services/org/org-dal.ts index b6262f2de..93d69d6e6 100644 --- a/backend-pg/src/services/org/org-dal.ts +++ b/backend-pg/src/services/org/org-dal.ts @@ -9,7 +9,7 @@ import { TOrgMembershipsUpdate } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { withTransaction } from "@app/lib/knex"; +import { ormify, withTransaction } from "@app/lib/knex"; export type TOrgDalFactory = ReturnType; @@ -110,14 +110,7 @@ export const orgDalFactory = (db: TDbClient) => { // MEMBERSHIP OPERATIONS // -------------------- - const findMembership = async (filter: Partial, tx?: Knex) => { - try { - const membership = await (tx || db)(TableName.OrgMembership).where(filter); - return membership; - } catch (error) { - throw new DatabaseError({ error, name: "Find org membership" }); - } - }; + const orgMembershipOrm = ormify(db, TableName.OrgMembership); const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => { try { @@ -175,7 +168,7 @@ export const orgDalFactory = (db: TDbClient) => { create, updateById, deleteById, - findMembership, + findMembership: orgMembershipOrm.find, createMembership, updateMembershipById, deleteMembershipById, diff --git a/backend-pg/src/services/org/org-service.ts b/backend-pg/src/services/org/org-service.ts index 979d24ffb..6978ea907 100644 --- a/backend-pg/src/services/org/org-service.ts +++ b/backend-pg/src/services/org/org-service.ts @@ -8,6 +8,8 @@ import { } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; +import { generateAsymmetricKeyPair } from "@app/lib/crypto"; +import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { isDisposableEmail } from "@app/lib/validator"; @@ -17,6 +19,7 @@ import { TokenType } from "../auth-token/auth-token-types"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDalFactory } from "../user/user-dal"; import { TIncidentContactsDalFactory } from "./incident-contacts-dal"; +import { TOrgBotDalFactory } from "./org-bot-dal"; import { TOrgDalFactory } from "./org-dal"; import { TOrgRoleDalFactory } from "./org-role-dal"; import { @@ -28,6 +31,7 @@ import { type TOrgServiceFactoryDep = { orgDal: TOrgDalFactory; + orgBotDal: TOrgBotDalFactory; orgRoleDal: TOrgRoleDalFactory; userDal: TUserDalFactory; incidentContactDal: TIncidentContactsDalFactory; @@ -45,7 +49,8 @@ export const orgServiceFactory = ({ incidentContactDal, permissionService, smtpService, - tokenService + tokenService, + orgBotDal }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id @@ -95,6 +100,23 @@ export const orgServiceFactory = ({ * Create organization * */ const createOrganization = async (userId: string, orgName: string) => { + const { privateKey, publicKey } = generateAsymmetricKeyPair(); + const key = generateSymmetricKey(); + const { + ciphertext: encryptedPrivateKey, + iv: privateKeyIV, + tag: privateKeyTag, + encoding: privateKeyKeyEncoding, + algorithm: privateKeyAlgorithm + } = infisicalSymmetricEncypt(privateKey); + const { + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + encoding: symmetricKeyKeyEncoding, + algorithm: symmetricKeyAlgorithm + } = infisicalSymmetricEncypt(key); + const organization = await orgDal.transaction(async (tx) => { const org = await orgDal.create({ name: orgName }, tx); await orgDal.createMembership( @@ -106,6 +128,24 @@ export const orgServiceFactory = ({ }, tx ); + await orgBotDal.create( + { + name: org.name, + publicKey, + privateKeyIV, + encryptedPrivateKey, + symmetricKeyIV, + symmetricKeyTag, + encryptedSymmetricKey, + symmetricKeyAlgorithm, + orgId: org.id, + privateKeyTag, + privateKeyAlgorithm, + privateKeyKeyEncoding, + symmetricKeyKeyEncoding + }, + tx + ); return org; }); @@ -178,7 +218,7 @@ export const orgServiceFactory = ({ // Thus the signup flow is not needed anymore const [inviteeMembership] = await orgDal.findMembership( { orgId, userId: inviteeUser.id }, - tx + { tx } ); if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) { throw new BadRequestError({ diff --git a/backend-pg/src/services/project-env/project-env-service.ts b/backend-pg/src/services/project-env/project-env-service.ts index ecb15cdf3..b3097f132 100644 --- a/backend-pg/src/services/project-env/project-env-service.ts +++ b/backend-pg/src/services/project-env/project-env-service.ts @@ -58,6 +58,9 @@ export const projectEnvServiceFactory = ({ ProjectPermissionSub.Environments ); + const oldEnv = await projectEnvDal.findOne({ id, projectId }); + if (!oldEnv) throw new BadRequestError({ message: "Environment not found" }); + if (slug) { const existingEnv = await projectEnvDal.findOne({ slug }); if (existingEnv && existingEnv.id !== id) { @@ -68,8 +71,8 @@ export const projectEnvServiceFactory = ({ } } - const [env] = await projectEnvDal.update({ id, projectId }, { name, slug }); - return env; + const env = await projectEnvDal.updateById(oldEnv.id, { name, slug }); + return { environment: env, old: oldEnv }; }; const deleteEnvironment = async ({ projectId, actor, actorId, id }: TDeleteEnvDTO) => { diff --git a/backend-pg/src/services/project-membership/project-membership-service.ts b/backend-pg/src/services/project-membership/project-membership-service.ts index f1ed76449..04b71e18a 100644 --- a/backend-pg/src/services/project-membership/project-membership-service.ts +++ b/backend-pg/src/services/project-membership/project-membership-service.ts @@ -8,6 +8,7 @@ import { } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; import { TOrgDalFactory } from "../org/org-dal"; import { TProjectDalFactory } from "../project/project-dal"; @@ -17,6 +18,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDalFactory } from "../user/user-dal"; import { TProjectMembershipDalFactory } from "./project-membership-dal"; import { + TAddUsersToWorkspaceDTO, TDeleteProjectMembershipDTO, TGetProjectMembershipDTO, TInviteUserToProjectDTO, @@ -31,7 +33,7 @@ type TProjectMembershipServiceFactoryDep = { projectRoleDal: Pick; orgDal: Pick; projectDal: Pick; - projectKeyDal: Pick; + projectKeyDal: Pick; }; export type TProjectMembershipServiceFactory = ReturnType; @@ -122,6 +124,71 @@ export const projectMembershipServiceFactory = ({ return { invitee, latestKey }; }; + const addUsersToProject = async ({ + projectId, + actorId, + actor, + members + }: TAddUsersToWorkspaceDTO) => { + const project = await projectDal.findById(projectId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Member + ); + const orgMembers = await orgDal.findMembership({ + orgId: project.orgId, + $in: { id: members.map(({ orgMembershipId }) => orgMembershipId) } + }); + if (orgMembers.length !== members.length) + throw new BadRequestError({ message: "Some users are not part of org" }); + + const existingMembers = await projectMembershipDal.find({ + projectId, + $in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) as string[] } + }); + if (existingMembers.length) + throw new BadRequestError({ message: "Some users are already part of project" }); + + await projectMembershipDal.transaction(async (tx) => { + await projectMembershipDal.insertMany( + orgMembers.map(({ userId }) => ({ + projectId, + userId: userId as string, + role: ProjectMembershipRole.Member + })), + tx + ); + const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); + await projectKeyDal.insertMany( + orgMembers.map(({ userId, id }) => ({ + encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, + nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, + senderId: actorId, + receiverId: userId as string, + projectId + })), + tx + ); + }); + const sender = await userDal.findById(actorId); + const appCfg = getConfig(); + await smtpService.sendMail({ + template: SmtpTemplates.WorkspaceInvite, + subjectLine: "Infisical workspace invitation", + recipients: orgMembers.map(({ userId }) => userId).filter(Boolean) as string[], + substitutions: { + inviterFirstName: sender.firstName, + inviterEmail: sender.email, + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } + }); + return orgMembers; + }; + const updateProjectMembership = async ({ actorId, actor, @@ -186,6 +253,7 @@ export const projectMembershipServiceFactory = ({ getProjectMemberships, inviteUserToProject, updateProjectMembership, - deleteProjectMembership + deleteProjectMembership, + addUsersToProject }; }; diff --git a/backend-pg/src/services/project-membership/project-membership-types.ts b/backend-pg/src/services/project-membership/project-membership-types.ts index 2acbf6064..6ee1d2965 100644 --- a/backend-pg/src/services/project-membership/project-membership-types.ts +++ b/backend-pg/src/services/project-membership/project-membership-types.ts @@ -14,3 +14,11 @@ export type TUpdateProjectMembershipDTO = { export type TDeleteProjectMembershipDTO = { membershipId: string; } & TProjectPermission; + +export type TAddUsersToWorkspaceDTO = { + members: { + orgMembershipId: string; + workspaceEncryptedKey: string; + workspaceEncryptedNonce: string; + }[]; +} & TProjectPermission; diff --git a/backend-pg/src/services/secret-folder/secret-folder-service.ts b/backend-pg/src/services/secret-folder/secret-folder-service.ts index 05acc1420..690e3da87 100644 --- a/backend-pg/src/services/secret-folder/secret-folder-service.ts +++ b/backend-pg/src/services/secret-folder/secret-folder-service.ts @@ -91,14 +91,16 @@ export const secretFolderServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + const parentFolder = await folderDal.findBySecretPath(projectId, environment, path); + if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); + const env = await projectEnvDal.findOne({ projectId, slug: environment }); if (!env) - throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); - - const folder = await folderDal.transaction(async (tx) => { - const parentFolder = await folderDal.findBySecretPath(projectId, environment, path, tx); - if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); + throw new BadRequestError({ message: "Environment not found", name: "Update folder" }); + const folder = await folderDal.findOne({ envId: env.id, id, parentId: parentFolder.id }); + if (!folder) throw new BadRequestError({ message: "Folder not found" }); + const newFolder = await folderDal.transaction(async (tx) => { const [doc] = await folderDal.update( { envId: env.id, id, parentId: parentFolder.id }, { name }, @@ -117,8 +119,8 @@ export const secretFolderServiceFactory = ({ return doc; }); - await snapshotService.performSnapshot(folder.parentId as string); - return folder; + await snapshotService.performSnapshot(newFolder.parentId as string); + return { folder: newFolder, old: folder }; }; const deleteFolder = async ({ diff --git a/backend-pg/src/services/secret-import/secret-import-service.ts b/backend-pg/src/services/secret-import/secret-import-service.ts index 11ada47ad..1f2c791c2 100644 --- a/backend-pg/src/services/secret-import/secret-import-service.ts +++ b/backend-pg/src/services/secret-import/secret-import-service.ts @@ -6,8 +6,10 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; import { TProjectEnvDalFactory } from "../project-env/project-env-dal"; +import { TSecretDalFactory } from "../secret/secret-dal"; import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDalFactory } from "./secret-import-dal"; import { @@ -17,8 +19,6 @@ import { TGetSecretsFromImportDTO, TUpdateSecretImportDTO } from "./secret-import-types"; -import { TSecretDalFactory } from "../secret/secret-dal"; -import { groupBy } from "@app/lib/fn"; type TSecretImportServiceFactoryDep = { secretImportDal: TSecretImportDalFactory; diff --git a/backend-pg/src/services/secret/secret-service.ts b/backend-pg/src/services/secret/secret-service.ts index 7fb76f620..45f119c74 100644 --- a/backend-pg/src/services/secret/secret-service.ts +++ b/backend-pg/src/services/secret/secret-service.ts @@ -256,9 +256,10 @@ export const secretServiceFactory = ({ } const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({ - inputSecrets: [{ secretName: inputSecret.secretName }], + inputSecrets: [{ secretName: inputSecret.secretName, type: inputSecret.type as SecretType }], folderId, isNew: true, + userId: actorId, blindIndexCfg }); @@ -539,6 +540,7 @@ export const secretServiceFactory = ({ fnSecretBulkInsert({ inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({ ...el, + version:0, secretBlindIndex: keyName2BlindIndex[secretName], type: SecretType.Shared, algorithm: SecretEncryptionAlgo.AES_256_GCM, diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 0a727441f..6df6a3933 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -18,7 +18,7 @@ export const useCreateProjectRole = () => { return useMutation({ mutationFn: ({ projectId, permissions, ...dto }: TCreateProjectRoleDTO) => - apiRequest.post(`/api/ee/v1/workspace/${projectId}/roles`, { + apiRequest.post(`/api/v1/workspace/${projectId}/roles`, { ...dto, permissions: permissions.length ? packRules(permissions) : [] }), @@ -33,7 +33,7 @@ export const useUpdateProjectRole = () => { return useMutation({ mutationFn: ({ id, projectId, permissions, ...dto }: TUpdateProjectRoleDTO) => - apiRequest.patch(`/api/ee/v1/workspace/${projectId}/roles/${id}`, { + apiRequest.patch(`/api/v1/workspace/${projectId}/roles/${id}`, { ...dto, permissions: permissions?.length ? packRules(permissions) : [] }), @@ -48,7 +48,7 @@ export const useDeleteProjectRole = () => { return useMutation({ mutationFn: ({ projectId, id }: TDeleteProjectRoleDTO) => - apiRequest.delete(`/api/ee/v1/workspace/${projectId}/roles/${id}`, { + apiRequest.delete(`/api/v1/workspace/${projectId}/roles/${id}`, { data: { projectId } }), onSuccess: (_, { projectId }) => { @@ -62,7 +62,7 @@ export const useCreateOrgRole = () => { return useMutation({ mutationFn: ({ orgId, permissions, ...dto }: TCreateOrgRoleDTO) => - apiRequest.post(`/api/ee/v1/organization/${orgId}/roles`, { + apiRequest.post(`/api/v1/organization/${orgId}/roles`, { ...dto, permissions: permissions.length ? packRules(permissions) : [] }), @@ -77,7 +77,7 @@ export const useUpdateOrgRole = () => { return useMutation({ mutationFn: ({ id, orgId, permissions, ...dto }: TUpdateOrgRoleDTO) => - apiRequest.patch(`/api/ee/v1/organization/${orgId}/roles/${id}`, { + apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, { ...dto, permissions: permissions?.length ? packRules(permissions) : [] }), @@ -92,7 +92,7 @@ export const useDeleteOrgRole = () => { return useMutation({ mutationFn: ({ orgId, id }: TDeleteOrgRoleDTO) => - apiRequest.delete(`/api/ee/v1/organization/${orgId}/roles/${id}`, { + apiRequest.delete(`/api/v1/organization/${orgId}/roles/${id}`, { data: { orgId } }), onSuccess: (_, { orgId }) => { diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index e735feb59..38d020491 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -48,7 +48,7 @@ export const roleQueryKeys = { const getProjectRoles = async (projectId: string) => { const { data } = await apiRequest.get<{ data: { roles: Array & { permissions: unknown }> }; - }>(`/api/ee/v1/workspace/${projectId}/roles`); + }>(`/api/v1/workspace/${projectId}/roles`); return data.data.roles.map(({ permissions, ...el }) => ({ ...el, permissions: unpackRules(permissions as PackRule[]) @@ -65,7 +65,7 @@ export const useGetProjectRoles = (projectId: string) => const getOrgRoles = async (orgId: string) => { const { data } = await apiRequest.get<{ data: { roles: Array & { permissions: unknown }> }; - }>(`/api/ee/v1/organization/${orgId}/roles`); + }>(`/api/v1/organization/${orgId}/roles`); return data.data.roles.map(({ permissions, ...el }) => ({ ...el, permissions: unpackRules(permissions as PackRule[]) @@ -85,7 +85,7 @@ const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { const { data } = await apiRequest.get<{ permissions: PackRule>>[]; membership: OrgUser; - }>(`/api/ee/v1/organization/${orgId}/permissions`); + }>(`/api/v1/organization/${orgId}/permissions`); return data; }; @@ -105,7 +105,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) = const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermissionDTO) => { const { data } = await apiRequest.get<{ data: { permissions: PackRule>>[] }; - }>(`/api/ee/v1/workspace/${workspaceId}/permissions`, {}); + }>(`/api/v1/workspace/${workspaceId}/permissions`, {}); return data.data.permissions; }; diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index 86b307cfe..e0a8df95d 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => { return useMutation<{}, {}, TCreateSecretPolicyDTO>({ mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => { - const { data } = await apiRequest.post("/api/ee/v1/secret-approvals", { + const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, workspaceId, approvals, @@ -31,7 +31,7 @@ export const useUpdateSecretApprovalPolicy = () => { return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ mutationFn: async ({ id, approvers, approvals, secretPath, name }) => { - const { data } = await apiRequest.patch(`/api/ee/v1/secret-approvals/${id}`, { + const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, approvers, secretPath, @@ -50,7 +50,7 @@ export const useDeleteSecretApprovalPolicy = () => { return useMutation<{}, {}, TDeleteSecretPolicyDTO>({ mutationFn: async ({ id }) => { - const { data } = await apiRequest.delete(`/api/ee/v1/secret-approvals/${id}`); + const { data } = await apiRequest.delete(`/api/v1/secret-approvals/${id}`); return data; }, onSuccess: (_, { workspaceId }) => { diff --git a/frontend/src/hooks/api/secretApproval/queries.tsx b/frontend/src/hooks/api/secretApproval/queries.tsx index db65737f3..ae90ff766 100644 --- a/frontend/src/hooks/api/secretApproval/queries.tsx +++ b/frontend/src/hooks/api/secretApproval/queries.tsx @@ -19,7 +19,7 @@ export const secretApprovalKeys = { const fetchApprovalPolicies = async (workspaceId: string) => { const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>( - "/api/ee/v1/secret-approvals", + "/api/v1/secret-approvals", { params: { workspaceId } } ); return data.approvals; @@ -49,7 +49,7 @@ const fetchApprovalPolicyOfABoard = async ( secretPath: string ) => { const { data } = await apiRequest.get<{ policy: TSecretApprovalPolicy }>( - "/api/ee/v1/secret-approvals/board", + "/api/v1/secret-approvals/board", { params: { workspaceId, environment, secretPath } } ); return data.policy || ""; diff --git a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx index 4eb84c352..94358ed79 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/ee/v1/secret-approval-requests/${id}/review`, { + const { data } = await apiRequest.post(`/api/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/ee/v1/secret-approval-requests/${id}/status`, { + const { data } = await apiRequest.post(`/api/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/ee/v1/secret-approval-requests/${id}/merge`); + const { data } = await apiRequest.post(`/api/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 94f4eda57..23017c15d 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -96,7 +96,7 @@ const fetchSecretApprovalRequestList = async ({ offset }: TGetSecretApprovalRequestList) => { const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>( - "/api/ee/v1/secret-approval-requests", + "/api/v1/secret-approval-requests", { params: { workspaceId, @@ -158,7 +158,7 @@ const fetchSecretApprovalRequestDetails = async ({ id }: Omit) => { const { data } = await apiRequest.get<{ approval: TSecretApprovalRequest }>( - `/api/ee/v1/secret-approval-requests/${id}` + `/api/v1/secret-approval-requests/${id}` ); return data.approval; @@ -197,7 +197,7 @@ export const useGetSecretApprovalRequestDetails = ({ const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => { const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>( - "/api/ee/v1/secret-approval-requests/count", + "/api/v1/secret-approval-requests/count", { params: { workspaceId } } ); diff --git a/frontend/src/hooks/api/secretRotation/mutation.tsx b/frontend/src/hooks/api/secretRotation/mutation.tsx index eb71e6e9d..cda4f8074 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/ee/v1/secret-rotations", dto); + const { data } = await apiRequest.post("/api/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/ee/v1/secret-rotations/${dto.id}`); + const { data } = await apiRequest.delete(`/api/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/ee/v1/secret-rotations/restart", { id: dto.id }); + const { data } = await apiRequest.post("/api/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 c3a107137..a3c8e270e 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/ee/v1/secret-rotation-providers/${workspaceId}` + `/api/v1/secret-rotation-providers/${workspaceId}` ); return data; }; @@ -55,7 +55,7 @@ const fetchSecretRotations = async ({ workspaceId }: Omit) => { const { data } = await apiRequest.get<{ secretRotations: TSecretRotation[] }>( - "/api/ee/v1/secret-rotations", + "/api/v1/secret-rotations", { params: { workspaceId } } ); return data.secretRotations; diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index cbbe7e743..ca1ec76fd 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -35,7 +35,7 @@ const fetchWorkspaceSnaphots = async ({ offset = 0 }: TGetSecretSnapshotsDTO & { offset: number }) => { const res = await apiRequest.get<{ secretSnapshots: TSecretSnapshot[] }>( - `/api/ee/v1/workspace/${workspaceId}/secret-snapshots`, + `/api/v1/workspace/${workspaceId}/secret-snapshots`, { params: { limit, @@ -60,7 +60,7 @@ export const useGetWorkspaceSnapshotList = (dto: TGetSecretSnapshotsDTO & { isPa const fetchSnapshotEncSecrets = async (snapshotId: string) => { const res = await apiRequest.get<{ secretSnapshot: TSnapshotData }>( - `/api/ee/v1/secret-snapshot/${snapshotId}` + `/api/v1/secret-snapshot/${snapshotId}` ); return res.data.secretSnapshot; }; @@ -142,7 +142,7 @@ const fetchWorkspaceSecretSnaphotCount = async ( directory = "/" ) => { const res = await apiRequest.get<{ count: number }>( - `/api/ee/v1/workspace/${workspaceId}/secret-snapshots/count`, + `/api/v1/workspace/${workspaceId}/secret-snapshots/count`, { params: { environment, @@ -170,7 +170,7 @@ export const usePerformSecretRollback = () => { return useMutation<{}, {}, TSecretRollbackDTO>({ mutationFn: async ({ snapshotId }) => { - const { data } = await apiRequest.post(`/api/ee/v1/secret-snapshot/${snapshotId}/rollback`); + const { data } = await apiRequest.post(`/api/v1/secret-snapshot/${snapshotId}/rollback`); return data; }, onSuccess: (_, { workspaceId, environment, directory }) => { diff --git a/frontend/src/hooks/api/ssoConfig/queries.tsx b/frontend/src/hooks/api/ssoConfig/queries.tsx index f3451060f..9b75095b8 100644 --- a/frontend/src/hooks/api/ssoConfig/queries.tsx +++ b/frontend/src/hooks/api/ssoConfig/queries.tsx @@ -3,22 +3,20 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; const ssoConfigKeys = { - getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const, -} + getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const +}; export const useGetSSOConfig = (organizationId: string) => { return useQuery({ queryKey: ssoConfigKeys.getSSOConfig(organizationId), queryFn: async () => { - const { data } = await apiRequest.get( - `/api/v1/sso/config?organizationId=${organizationId}` - ); + const { data } = await apiRequest.get(`/api/v1/sso/config?organizationId=${organizationId}`); return data; }, enabled: true }); -} +}; export const useCreateSSOConfig = () => { const queryClient = useQueryClient(); @@ -38,18 +36,15 @@ export const useCreateSSOConfig = () => { issuer: string; cert: string; }) => { - const { data } = await apiRequest.post( - "/api/v1/sso/config", - { - organizationId, - authProvider, - isActive, - entryPoint, - issuer, - cert - } - ); - + const { data } = await apiRequest.post("/api/v1/sso/config", { + organizationId, + authProvider, + isActive, + entryPoint, + issuer, + cert + }); + return data; }, onSuccess(_, dto) { @@ -76,22 +71,19 @@ export const useUpdateSSOConfig = () => { issuer?: string; cert?: string; }) => { - const { data } = await apiRequest.patch( - "/api/v1/sso/config", - { - organizationId, - ...(authProvider !== undefined ? { authProvider } : {}), - ...(isActive !== undefined ? { isActive } : {}), - ...(entryPoint !== undefined ? { entryPoint } : {}), - ...(issuer !== undefined ? { issuer } : {}), - ...(cert !== undefined ? { cert } : {}) - } - ); - + const { data } = await apiRequest.patch("/api/v1/sso/config", { + organizationId, + ...(authProvider !== undefined ? { authProvider } : {}), + ...(isActive !== undefined ? { isActive } : {}), + ...(entryPoint !== undefined ? { entryPoint } : {}), + ...(issuer !== undefined ? { issuer } : {}), + ...(cert !== undefined ? { cert } : {}) + }); + return data; }, onSuccess(_, dto) { queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId)); } }); -}; \ No newline at end of file +};