diff --git a/.infisicalignore b/.infisicalignore index 4ccf734b6..2c52b2b4f 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -24,3 +24,5 @@ frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26 docs/documentation/platform/kms/overview.mdx:generic-api-key:281 docs/documentation/platform/kms/overview.mdx:generic-api-key:344 +docs/cli/commands/user.mdx:generic-api-key:51 +frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76 \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index eaf32ae4c..19e10942e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -59,6 +59,7 @@ "axios": "^1.6.7", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", + "botbuilder": "^4.23.2", "bullmq": "^5.4.2", "cassandra-driver": "^4.7.2", "connect-redis": "^7.1.1", @@ -2358,12 +2359,13 @@ } }, "node_modules/@azure/core-auth": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", - "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.1.0", + "@azure/core-util": "^1.11.0", "tslib": "^2.6.2" }, "engines": { @@ -2518,14 +2520,15 @@ } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.1.tgz", - "integrity": "sha512-ExPSbgjwCoht6kB7B4MeZoBAxcQSIl29r/bPeazZJx50ej4JJCByimLOrZoIsurISNyJQQHf30b3JfqC3Hb88A==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz", + "integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==", + "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.4.0", + "@azure/core-auth": "^1.8.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.9.0", + "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", @@ -2602,9 +2605,10 @@ } }, "node_modules/@azure/core-util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", - "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", "tslib": "^2.6.2" @@ -2625,46 +2629,60 @@ } }, "node_modules/@azure/identity": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.3.0.tgz", - "integrity": "sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz", + "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==", + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.5.0", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", - "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^3.11.1", - "@azure/msal-node": "^2.9.2", - "events": "^3.0.0", - "jws": "^4.0.0", - "open": "^8.0.0", - "stoppable": "^1.1.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", "tslib": "^2.2.0" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@azure/identity/node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "node_modules/@azure/identity/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@azure/identity/node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "node_modules/@azure/identity/node_modules/@azure/msal-node": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz", + "integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==", + "license": "MIT", "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" + "@azure/msal-common": "15.5.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/identity/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/@azure/keyvault-keys": { @@ -2700,30 +2718,33 @@ } }, "node_modules/@azure/msal-browser": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.18.0.tgz", - "integrity": "sha512-jvK5bDUWbpOaJt2Io/rjcaOVcUzkqkrCme/WntdV1SMUc67AiTcEdKuY6G/nMQ7N5Cfsk9SfpugflQwDku53yg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz", + "integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==", + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.13.0" + "@azure/msal-common": "15.5.1" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "14.13.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.13.0.tgz", - "integrity": "sha512-b4M/tqRzJ4jGU91BiwCsLTqChveUEyFK3qY2wGfZ0zBswIBZjAxopx5CYt5wzZFKuN15HqRDYXQbztttuIC3nA==", + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz", + "integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==", + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.10.0.tgz", - "integrity": "sha512-JxsSE0464a8IA/+q5EHKmchwNyUFJHtCH00tSXsLaOddwLjG6yVvTH6lGgPcWMhO7YWUXj/XVgVgeE9kZtsPUQ==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", + "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.13.0", + "@azure/msal-common": "14.16.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -2731,6 +2752,15 @@ "node": ">=16" } }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@azure/msal-node/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -10011,9 +10041,10 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", - "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -10398,6 +10429,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz", + "integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/xml-encryption": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz", @@ -11150,6 +11190,12 @@ "node": ">=0.4.0" } }, + "node_modules/adaptivecards": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/adaptivecards/-/adaptivecards-1.2.3.tgz", + "integrity": "sha512-amQ5OSW3OpIkrxVKLjxVBPk/T49yuOtnqs1z5ZPfZr0+OpTovzmiHbyoAGDIsu5SNYHwOZFp/3LGOnRaALFa/g==", + "license": "MIT" + }, "node_modules/adm-zip": { "version": "0.5.12", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz", @@ -12012,6 +12058,245 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/botbuilder": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.2.tgz", + "integrity": "sha512-E3UjkPlAmT8TidZIAW1ucVRejz0KBbWEn0wNxJ37GncPl8txhmWvs21xn3hBrifSR4++Y6q/hp/A5cHFQcFGJw==", + "license": "MIT", + "dependencies": { + "@azure/core-http": "^3.0.4", + "@azure/msal-node": "^2.13.1", + "axios": "^1.7.7", + "botbuilder-core": "4.23.2", + "botbuilder-stdlib": "4.23.2-internal", + "botframework-connector": "4.23.2", + "botframework-schema": "4.23.2", + "botframework-streaming": "4.23.2", + "dayjs": "^1.11.13", + "filenamify": "^6.0.0", + "fs-extra": "^11.2.0", + "htmlparser2": "^9.0.1", + "uuid": "^10.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/botbuilder-core": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/botbuilder-core/-/botbuilder-core-4.23.2.tgz", + "integrity": "sha512-GwrfkfbEJqCLnhDVc6uKlzKtrptfYTxQxHYfF22s1AxTKdTiA9vsDN9rXq8We7QUPXFOF1ylF1e87k0fQ3Sf+A==", + "license": "MIT", + "dependencies": { + "botbuilder-dialogs-adaptive-runtime-core": "4.23.2-preview", + "botbuilder-stdlib": "4.23.2-internal", + "botframework-connector": "4.23.2", + "botframework-schema": "4.23.2", + "uuid": "^10.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/botbuilder-core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/botbuilder-dialogs-adaptive-runtime-core": { + "version": "4.23.2-preview", + "resolved": "https://registry.npmjs.org/botbuilder-dialogs-adaptive-runtime-core/-/botbuilder-dialogs-adaptive-runtime-core-4.23.2-preview.tgz", + "integrity": "sha512-+b5oHSDNodYXPnQbub+hTNmQLtBB4hj/ZW73g4Sqv5oAdqHoK/dX181UpiFAvDpHGe8Kx3SNYtRHJIj71u4t0Q==", + "license": "MIT", + "dependencies": { + "dependency-graph": "^1.0.0" + } + }, + "node_modules/botbuilder-stdlib": { + "version": "4.23.2-internal", + "resolved": "https://registry.npmjs.org/botbuilder-stdlib/-/botbuilder-stdlib-4.23.2-internal.tgz", + "integrity": "sha512-5WAu59gCZX3lz2NNw28q+IlAAFIQjXij0wXmN8qh+Tg4PQOCl+5P3hoYqcHIWtGd5Kgn+dpaHtBIewl2LaOXKQ==", + "license": "MIT" + }, + "node_modules/botbuilder/node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/botbuilder/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/botframework-connector": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/botframework-connector/-/botframework-connector-4.23.2.tgz", + "integrity": "sha512-G4gDpEHhA8AUKbgHMJ1LUjsuDlRPFEcXnH8ouxLI0opT2p1LcUSAAgS4hoOrkaylr04zxrUI0nEWkuWDiWDwzw==", + "license": "MIT", + "dependencies": { + "@azure/core-http": "^3.0.4", + "@azure/identity": "^4.4.1", + "@azure/msal-node": "^2.13.1", + "@types/jsonwebtoken": "9.0.6", + "axios": "^1.7.7", + "base64url": "^3.0.0", + "botbuilder-stdlib": "4.23.2-internal", + "botframework-schema": "4.23.2", + "buffer": "^6.0.3", + "cross-fetch": "^4.0.0", + "https-proxy-agent": "^7.0.5", + "jsonwebtoken": "^9.0.2", + "node-fetch": "^2.7.0", + "openssl-wrapper": "^0.3.4", + "rsa-pem-from-mod-exp": "^0.8.6", + "zod": "^3.23.8" + } + }, + "node_modules/botframework-connector/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/botframework-connector/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/botframework-connector/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/botframework-schema": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/botframework-schema/-/botframework-schema-4.23.2.tgz", + "integrity": "sha512-eO1fmvfCEVJfnqNNAerQU8CHp0FMYTyE459ztNx2k1QJYMl/ds+LNNkGIUlQQFsdVbi2umadK+6hL2a9kqXMqQ==", + "license": "MIT", + "dependencies": { + "adaptivecards": "1.2.3", + "uuid": "^10.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/botframework-schema/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/botframework-streaming": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/botframework-streaming/-/botframework-streaming-4.23.2.tgz", + "integrity": "sha512-UBF0puC2RX8Z0dkN/ag9BuSNWdB5MUtobZLzeaH1h5t7QAYAVNk/SrsUgkBwUsqWpwaZqU+vrGOeByLShDcvaQ==", + "license": "MIT", + "dependencies": { + "@types/node": "18.19.47", + "@types/ws": "^6.0.3", + "uuid": "^10.0.0", + "ws": "^7.5.10" + } + }, + "node_modules/botframework-streaming/node_modules/@types/node": { + "version": "18.19.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/botframework-streaming/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==", + "license": "MIT" + }, + "node_modules/botframework-streaming/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/botframework-streaming/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -12139,6 +12424,21 @@ "uuid": "^9.0.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bundle-require": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz", @@ -12791,6 +13091,15 @@ "node": ">=12.0.0" } }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -12872,6 +13181,12 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/dc-polyfill": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz", @@ -13011,6 +13326,34 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -13029,11 +13372,15 @@ } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-properties": { @@ -13093,6 +13440,15 @@ "node": ">= 0.8" } }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -13166,6 +13522,47 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/dompurify": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", @@ -13174,6 +13571,20 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", @@ -14494,6 +14905,33 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -15678,6 +16116,25 @@ ], "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -16191,6 +16648,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -16718,7 +17208,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -18767,16 +19256,33 @@ } }, "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz", + "integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==", + "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -18801,6 +19307,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/openssl-wrapper": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/openssl-wrapper/-/openssl-wrapper-0.3.4.tgz", + "integrity": "sha512-iITsrx6Ho8V3/2OVtmZzzX8wQaKAaFXEJQdzoPUZDtyf5jWFlqo+h+OhGT4TATQ47f9ACKHua8nw7Qoy85aeKQ==", + "license": "MIT" + }, "node_modules/opentracing": { "version": "0.14.7", "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz", @@ -20854,6 +21366,24 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==" }, + "node_modules/rsa-pem-from-mod-exp": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.6.tgz", + "integrity": "sha512-c5ouQkOvGHF1qomUUDJGFcXsomeSO2gbEs6hVhMAtlkE1CuaZase/WzoaKFG/EZQuNmq6pw/EMCeEnDvOgCJYQ==", + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -21728,15 +22258,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, "node_modules/stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", @@ -23520,7 +24041,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -24951,9 +25471,10 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/backend/package.json b/backend/package.json index 5db1dffe0..58bc6a32d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -175,6 +175,7 @@ "axios": "^1.6.7", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", + "botbuilder": "^4.23.2", "bullmq": "^5.4.2", "cassandra-driver": "^4.7.2", "connect-redis": "^7.1.1", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 34d816b9b..92c22874e 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -100,6 +100,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; declare module "@fastify/request-context" { interface RequestContextData { @@ -246,6 +247,7 @@ declare module "fastify" { kmipOperation: TKmipOperationServiceFactory; gateway: TGatewayServiceFactory; secretRotationV2: TSecretRotationV2ServiceFactory; + microsoftTeams: TMicrosoftTeamsServiceFactory; assumePrivileges: TAssumePrivilegeServiceFactory; githubOrgSync: TGithubOrgSyncServiceFactory; }; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 091199938..7ac57afb2 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -426,6 +426,16 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; +import { + TMicrosoftTeamsIntegrations, + TMicrosoftTeamsIntegrationsInsert, + TMicrosoftTeamsIntegrationsUpdate +} from "@app/db/schemas/microsoft-teams-integrations"; +import { + TProjectMicrosoftTeamsConfigs, + TProjectMicrosoftTeamsConfigsInsert, + TProjectMicrosoftTeamsConfigsUpdate +} from "@app/db/schemas/project-microsoft-teams-configs"; import { TSecretReminderRecipients, TSecretReminderRecipientsInsert, @@ -1002,6 +1012,16 @@ declare module "knex/types/tables" { TSecretRotationV2SecretMappingsInsert, TSecretRotationV2SecretMappingsUpdate >; + [TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType< + TMicrosoftTeamsIntegrations, + TMicrosoftTeamsIntegrationsInsert, + TMicrosoftTeamsIntegrationsUpdate + >; + [TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType< + TProjectMicrosoftTeamsConfigs, + TProjectMicrosoftTeamsConfigsInsert, + TProjectMicrosoftTeamsConfigsUpdate + >; [TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType< TSecretReminderRecipients, TSecretReminderRecipientsInsert, diff --git a/backend/src/db/migrations/20250422125635_microsoft-teams-workflow-integration.ts b/backend/src/db/migrations/20250422125635_microsoft-teams-workflow-integration.ts new file mode 100644 index 000000000..ed1b9333c --- /dev/null +++ b/backend/src/db/migrations/20250422125635_microsoft-teams-workflow-integration.ts @@ -0,0 +1,130 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const superAdminHasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedMicrosoftTeamsAppId" + ); + const superAdminHasEncryptedMicrosoftTeamsClientSecret = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedMicrosoftTeamsClientSecret" + ); + const superAdminHasEncryptedMicrosoftTeamsBotId = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedMicrosoftTeamsBotId" + ); + + if ( + !superAdminHasEncryptedMicrosoftTeamsClientIdColumn || + !superAdminHasEncryptedMicrosoftTeamsClientSecret || + !superAdminHasEncryptedMicrosoftTeamsBotId + ) { + await knex.schema.alterTable(TableName.SuperAdmin, (table) => { + if (!superAdminHasEncryptedMicrosoftTeamsClientIdColumn) { + table.binary("encryptedMicrosoftTeamsAppId").nullable(); + } + if (!superAdminHasEncryptedMicrosoftTeamsClientSecret) { + table.binary("encryptedMicrosoftTeamsClientSecret").nullable(); + } + if (!superAdminHasEncryptedMicrosoftTeamsBotId) { + table.binary("encryptedMicrosoftTeamsBotId").nullable(); + } + }); + } + + if (!(await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status"))) { + await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => { + table.enu("status", ["pending", "installed", "failed"]).notNullable().defaultTo("installed"); // defaults to installed so we can have backwards compatibility with existing workflow integrations + }); + } + + if (!(await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations))) { + await knex.schema.createTable(TableName.MicrosoftTeamsIntegrations, (table) => { + table.uuid("id", { primaryKey: true }).notNullable(); + table.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE"); // the ID itself is the workflow integration ID + + table.string("internalTeamsAppId").nullable(); + table.string("tenantId").notNullable(); + table.binary("encryptedAccessToken").nullable(); + table.binary("encryptedBotAccessToken").nullable(); + + table.timestamp("accessTokenExpiresAt").nullable(); + table.timestamp("botAccessTokenExpiresAt").nullable(); + + table.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations); + } + + if (!(await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs))) { + await knex.schema.createTable(TableName.ProjectMicrosoftTeamsConfigs, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.string("projectId").notNullable().unique(); + tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + tb.uuid("microsoftTeamsIntegrationId").notNullable(); + tb.foreign("microsoftTeamsIntegrationId") + .references("id") + .inTable(TableName.MicrosoftTeamsIntegrations) + .onDelete("CASCADE"); + tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false); + tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false); + + tb.jsonb("accessRequestChannels").notNullable(); // {teamId: string, channelIds: string[]} + tb.jsonb("secretRequestChannels").notNullable(); // {teamId: string, channelIds: string[]} + tb.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedMicrosoftTeamsAppId" + ); + const hasEncryptedMicrosoftTeamsClientSecret = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedMicrosoftTeamsClientSecret" + ); + const hasEncryptedMicrosoftTeamsBotId = await knex.schema.hasColumn( + TableName.SuperAdmin, + "encryptedMicrosoftTeamsBotId" + ); + + if ( + hasEncryptedMicrosoftTeamsClientIdColumn || + hasEncryptedMicrosoftTeamsClientSecret || + hasEncryptedMicrosoftTeamsBotId + ) { + await knex.schema.alterTable(TableName.SuperAdmin, (table) => { + if (hasEncryptedMicrosoftTeamsClientIdColumn) { + table.dropColumn("encryptedMicrosoftTeamsAppId"); + } + if (hasEncryptedMicrosoftTeamsClientSecret) { + table.dropColumn("encryptedMicrosoftTeamsClientSecret"); + } + if (hasEncryptedMicrosoftTeamsBotId) { + table.dropColumn("encryptedMicrosoftTeamsBotId"); + } + }); + } + if (await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status")) { + await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => { + table.dropColumn("status"); + }); + } + + if (await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs)) { + await knex.schema.dropTableIfExists(TableName.ProjectMicrosoftTeamsConfigs); + await dropOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs); + } + if (await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations)) { + await knex.schema.dropTableIfExists(TableName.MicrosoftTeamsIntegrations); + await dropOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations); + } +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 7ccd71376..a69a6463c 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -58,6 +58,7 @@ export * from "./kms-keys"; export * from "./kms-root-config"; export * from "./ldap-configs"; export * from "./ldap-group-maps"; +export * from "./microsoft-teams-integrations"; export * from "./models"; export * from "./oidc-configs"; export * from "./org-bots"; diff --git a/backend/src/db/schemas/microsoft-teams-integrations.ts b/backend/src/db/schemas/microsoft-teams-integrations.ts new file mode 100644 index 000000000..36aae5f78 --- /dev/null +++ b/backend/src/db/schemas/microsoft-teams-integrations.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 { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const MicrosoftTeamsIntegrationsSchema = z.object({ + id: z.string().uuid(), + internalTeamsAppId: z.string().nullable().optional(), + tenantId: z.string(), + encryptedAccessToken: zodBuffer.nullable().optional(), + encryptedBotAccessToken: zodBuffer.nullable().optional(), + accessTokenExpiresAt: z.date().nullable().optional(), + botAccessTokenExpiresAt: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TMicrosoftTeamsIntegrations = z.infer; +export type TMicrosoftTeamsIntegrationsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TMicrosoftTeamsIntegrationsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index dd23c26da..e5af10cd6 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -147,6 +147,8 @@ export enum TableName { KmipClientCertificates = "kmip_client_certificates", SecretRotationV2 = "secret_rotations_v2", SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings", + MicrosoftTeamsIntegrations = "microsoft_teams_integrations", + ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs", SecretReminderRecipients = "secret_reminder_recipients", GithubOrgSyncConfig = "github_org_sync_configs" } diff --git a/backend/src/db/schemas/project-microsoft-teams-configs.ts b/backend/src/db/schemas/project-microsoft-teams-configs.ts new file mode 100644 index 000000000..27d0f7ef3 --- /dev/null +++ b/backend/src/db/schemas/project-microsoft-teams-configs.ts @@ -0,0 +1,29 @@ +// 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 ProjectMicrosoftTeamsConfigsSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + microsoftTeamsIntegrationId: z.string().uuid(), + isAccessRequestNotificationEnabled: z.boolean().default(false), + isSecretRequestNotificationEnabled: z.boolean().default(false), + accessRequestChannels: z.unknown(), + secretRequestChannels: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TProjectMicrosoftTeamsConfigs = z.infer; +export type TProjectMicrosoftTeamsConfigsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TProjectMicrosoftTeamsConfigsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 01aac280b..ec35042ad 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -26,7 +26,10 @@ export const SuperAdminSchema = z.object({ encryptedSlackClientSecret: zodBuffer.nullable().optional(), authConsentContent: z.string().nullable().optional(), pageFrameContent: z.string().nullable().optional(), - adminIdentityIds: z.string().array().nullable().optional() + adminIdentityIds: z.string().array().nullable().optional(), + encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(), + encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(), + encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/db/schemas/workflow-integrations.ts b/backend/src/db/schemas/workflow-integrations.ts index ae1ae9a25..cab02fced 100644 --- a/backend/src/db/schemas/workflow-integrations.ts +++ b/backend/src/db/schemas/workflow-integrations.ts @@ -14,7 +14,8 @@ export const WorkflowIntegrationsSchema = z.object({ orgId: z.string().uuid(), description: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + status: z.string().default("installed") }); export type TWorkflowIntegrations = z.infer; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 3606b4bdc..2b2758b2e 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -6,13 +6,15 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; +import { TriggerFeature } from "@app/lib/workflow-integrations/types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; +import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; -import { triggerSlackNotification } from "@app/services/slack/slack-fns"; -import { SlackTriggerFeature } from "@app/services/slack/slack-types"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -67,6 +69,8 @@ type TSecretApprovalRequestServiceFactoryDep = { >; kmsService: Pick; projectSlackConfigDAL: Pick; + microsoftTeamsService: Pick; + projectMicrosoftTeamsConfigDAL: Pick; }; export type TAccessApprovalRequestServiceFactory = ReturnType; @@ -84,6 +88,8 @@ export const accessApprovalRequestServiceFactory = ({ smtpService, userDAL, kmsService, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL, projectSlackConfigDAL }: TSecretApprovalRequestServiceFactoryDep) => { const createAccessApprovalRequest = async ({ @@ -219,24 +225,30 @@ export const accessApprovalRequestServiceFactory = ({ const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`; - await triggerSlackNotification({ - projectId: project.id, - projectSlackConfigDAL, - projectDAL, - kmsService, - notification: { - type: SlackTriggerFeature.ACCESS_REQUEST, - payload: { - projectName: project.name, - requesterFullName, - isTemporary, - requesterEmail: requestedByUser.email as string, - secretPath, - environment: envSlug, - permissions: accessTypes, - approvalUrl, - note - } + await triggerWorkflowIntegrationNotification({ + input: { + notification: { + type: TriggerFeature.ACCESS_REQUEST, + payload: { + projectName: project.name, + requesterFullName, + isTemporary, + requesterEmail: requestedByUser.email as string, + secretPath, + environment: envSlug, + permissions: accessTypes, + approvalUrl, + note + } + }, + projectId: project.id + }, + dependencies: { + projectDAL, + projectSlackConfigDAL, + kmsService, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL } }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b6527f3f4..9142212a4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -29,6 +29,7 @@ import { TSecretSyncRaw, TUpdateSecretSyncDTO } from "@app/services/secret-sync/secret-sync-types"; +import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types"; import { KmipPermission } from "../kmip/kmip-enum"; import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; @@ -244,11 +245,14 @@ export enum EventType { GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", + GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", + UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", GET_SLACK_INTEGRATION = "get-slack-integration", UPDATE_SLACK_INTEGRATION = "update-slack-integration", DELETE_SLACK_INTEGRATION = "delete-slack-integration", - GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", - UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", + GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config", + UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config", + GET_PROJECT_SSH_CONFIG = "get-project-ssh-config", UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config", INTEGRATION_SYNCED = "integration-synced", @@ -321,6 +325,15 @@ export enum EventType { SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets", PROJECT_ACCESS_REQUEST = "project-access-request", + + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list", + PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start", PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end" } @@ -1980,22 +1993,24 @@ interface GetSlackIntegration { }; } -interface UpdateProjectSlackConfig { - type: EventType.UPDATE_PROJECT_SLACK_CONFIG; +interface UpdateProjectWorkflowIntegrationConfig { + type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG; metadata: { id: string; - slackIntegrationId: string; + integrationId: string; + integration: WorkflowIntegration; isAccessRequestNotificationEnabled: boolean; - accessRequestChannels: string; + accessRequestChannels?: string | { teamId: string; channelIds: string[] }; isSecretRequestNotificationEnabled: boolean; - secretRequestChannels: string; + secretRequestChannels?: string | { teamId: string; channelIds: string[] }; }; } -interface GetProjectSlackConfig { - type: EventType.GET_PROJECT_SLACK_CONFIG; +interface GetProjectWorkflowIntegrationConfig { + type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG; metadata: { id: string; + integration: WorkflowIntegration; }; } @@ -2561,6 +2576,66 @@ interface RotateSecretRotationEvent { }; } +interface MicrosoftTeamsWorkflowIntegrationCreateEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE; + metadata: { + tenantId: string; + slug: string; + description?: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationDeleteEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE; + metadata: { + tenantId: string; + id: string; + slug: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS; + metadata: { + tenantId: string; + slug: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS; + metadata: { + tenantId: string; + slug: string; + id: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationGetEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET; + metadata: { + tenantId: string; + slug: string; + id: string; + }; +} + +interface MicrosoftTeamsWorkflowIntegrationListEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST; + metadata: Record; +} + +interface MicrosoftTeamsWorkflowIntegrationUpdateEvent { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE; + metadata: { + tenantId: string; + slug: string; + id: string; + newSlug?: string; + newDescription?: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2723,8 +2798,8 @@ export type Event = | UpdateSlackIntegration | DeleteSlackIntegration | GetSlackIntegration - | UpdateProjectSlackConfig - | GetProjectSlackConfig + | UpdateProjectWorkflowIntegrationConfig + | GetProjectWorkflowIntegrationConfig | GetProjectSshConfig | UpdateProjectSshConfig | IntegrationSyncedEvent @@ -2794,4 +2869,11 @@ export type Event = | CreateSecretRotationEvent | UpdateSecretRotationEvent | DeleteSecretRotationEvent - | RotateSecretRotationEvent; + | RotateSecretRotationEvent + | MicrosoftTeamsWorkflowIntegrationCreateEvent + | MicrosoftTeamsWorkflowIntegrationDeleteEvent + | MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent + | MicrosoftTeamsWorkflowIntegrationGetTeamsEvent + | MicrosoftTeamsWorkflowIntegrationGetEvent + | MicrosoftTeamsWorkflowIntegrationListEvent + | MicrosoftTeamsWorkflowIntegrationUpdateEvent; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 2f340626b..262e8e5cf 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -17,9 +17,13 @@ import { groupBy, pick, unique } from "@app/lib/fn"; import { setKnexStringValue } from "@app/lib/knex"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { EnforcementLevel } from "@app/lib/types"; +import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; +import { TriggerFeature } from "@app/lib/workflow-integrations/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; +import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; @@ -52,8 +56,6 @@ import { import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; -import { triggerSlackNotification } from "@app/services/slack/slack-fns"; -import { SlackTriggerFeature } from "@app/services/slack/slack-types"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -126,6 +128,8 @@ type TSecretApprovalRequestServiceFactoryDep = { secretApprovalPolicyDAL: Pick; projectSlackConfigDAL: Pick; licenseService: Pick; + projectMicrosoftTeamsConfigDAL: Pick; + microsoftTeamsService: Pick; }; export type TSecretApprovalRequestServiceFactory = ReturnType; @@ -155,7 +159,9 @@ export const secretApprovalRequestServiceFactory = ({ secretVersionTagV2BridgeDAL, licenseService, projectSlackConfigDAL, - resourceMetadataDAL + resourceMetadataDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); @@ -1171,21 +1177,28 @@ export const secretApprovalRequestServiceFactory = ({ const env = await projectEnvDAL.findOne({ id: policy.envId }); const user = await userDAL.findById(secretApprovalRequest.committerUserId); - await triggerSlackNotification({ - projectId, - projectDAL, - kmsService, - projectSlackConfigDAL, - notification: { - type: SlackTriggerFeature.SECRET_APPROVAL, - payload: { - userEmail: user.email as string, - environment: env.name, - secretPath, - projectId, - requestId: secretApprovalRequest.id, - secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))] + + await triggerWorkflowIntegrationNotification({ + input: { + projectId, + notification: { + type: TriggerFeature.SECRET_APPROVAL, + payload: { + userEmail: user.email as string, + environment: env.name, + secretPath, + projectId, + requestId: secretApprovalRequest.id, + secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))] + } } + }, + dependencies: { + projectDAL, + projectSlackConfigDAL, + kmsService, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService } }); @@ -1503,21 +1516,28 @@ export const secretApprovalRequestServiceFactory = ({ const user = await userDAL.findById(secretApprovalRequest.committerUserId); const env = await projectEnvDAL.findOne({ id: policy.envId }); - await triggerSlackNotification({ - projectId, - projectDAL, - kmsService, - projectSlackConfigDAL, - notification: { - type: SlackTriggerFeature.SECRET_APPROVAL, - payload: { - userEmail: user.email as string, - environment: env.name, - secretPath, - projectId, - requestId: secretApprovalRequest.id, - secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))] + + await triggerWorkflowIntegrationNotification({ + input: { + projectId, + notification: { + type: TriggerFeature.SECRET_APPROVAL, + payload: { + userEmail: user.email as string, + environment: env.name, + secretPath, + projectId, + requestId: secretApprovalRequest.id, + secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))] + } } + }, + dependencies: { + projectDAL, + kmsService, + projectSlackConfigDAL, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL } }); diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-schemas.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-schemas.ts index b1be4ea22..d7b885c92 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-schemas.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-schemas.ts @@ -20,7 +20,7 @@ export const BaseSecretRotationSchema = (type: SecretRotation) => // unique to provider type: true, parameters: true, - secretMappings: true + secretsMapping: true }).extend({ connection: z.object({ app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]), diff --git a/backend/src/lib/workflow-integrations/trigger-notification.ts b/backend/src/lib/workflow-integrations/trigger-notification.ts new file mode 100644 index 000000000..58411bdb0 --- /dev/null +++ b/backend/src/lib/workflow-integrations/trigger-notification.ts @@ -0,0 +1,98 @@ +import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; +import { sendSlackNotification } from "@app/services/slack/slack-fns"; + +import { logger } from "../logger"; +import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types"; + +export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => { + try { + const { projectId, notification } = dto.input; + const { projectDAL, projectSlackConfigDAL, kmsService, projectMicrosoftTeamsConfigDAL, microsoftTeamsService } = + dto.dependencies; + + const project = await projectDAL.findById(projectId); + + if (!project) { + return; + } + + const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId); + const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId); + + if (slackConfig) { + if (notification.type === TriggerFeature.ACCESS_REQUEST) { + const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; + if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) { + await sendSlackNotification({ + orgId: project.orgId, + notification, + kmsService, + targetChannelIds, + slackIntegration: slackConfig + }).catch((error) => { + logger.error(error, "Error sending Slack notification"); + }); + } + } else if (notification.type === TriggerFeature.SECRET_APPROVAL) { + const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; + if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) { + await sendSlackNotification({ + orgId: project.orgId, + notification, + kmsService, + targetChannelIds, + slackIntegration: slackConfig + }).catch((error) => { + logger.error(error, "Error sending Slack notification"); + }); + } + } + } + + if (microsoftTeamsConfig) { + if (notification.type === TriggerFeature.ACCESS_REQUEST) { + if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) { + const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse( + microsoftTeamsConfig.accessRequestChannels + ); + + if (success && data) { + await microsoftTeamsService + .sendNotification({ + notification, + target: data, + tenantId: microsoftTeamsConfig.tenantId, + microsoftTeamsIntegrationId: microsoftTeamsConfig.id, + orgId: project.orgId + }) + .catch((error) => { + logger.error(error, "Error sending Microsoft Teams notification"); + }); + } + } + } else if (notification.type === TriggerFeature.SECRET_APPROVAL) { + if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) { + const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse( + microsoftTeamsConfig.secretRequestChannels + ); + + if (success && data) { + await microsoftTeamsService + .sendNotification({ + notification, + target: data, + tenantId: microsoftTeamsConfig.tenantId, + microsoftTeamsIntegrationId: microsoftTeamsConfig.id, + orgId: project.orgId + }) + .catch((error) => { + logger.error(error, "Error sending Microsoft Teams notification"); + }); + } + } + } + } + } catch (error) { + logger.error(error, "Error triggering workflow integration notification"); + } +}; diff --git a/backend/src/lib/workflow-integrations/types.ts b/backend/src/lib/workflow-integrations/types.ts new file mode 100644 index 000000000..c18ecb496 --- /dev/null +++ b/backend/src/lib/workflow-integrations/types.ts @@ -0,0 +1,51 @@ +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; +import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; + +export enum TriggerFeature { + SECRET_APPROVAL = "secret-approval", + ACCESS_REQUEST = "access-request" +} + +export type TNotification = + | { + type: TriggerFeature.SECRET_APPROVAL; + payload: { + userEmail: string; + environment: string; + secretPath: string; + requestId: string; + projectId: string; + secretKeys: string[]; + }; + } + | { + type: TriggerFeature.ACCESS_REQUEST; + payload: { + requesterFullName: string; + requesterEmail: string; + isTemporary: boolean; + secretPath: string; + environment: string; + projectName: string; + permissions: string[]; + approvalUrl: string; + note?: string; + }; + }; + +export type TTriggerWorkflowNotificationDTO = { + input: { + projectId: string; + notification: TNotification; + }; + dependencies: { + projectDAL: Pick; + projectSlackConfigDAL: Pick; + projectMicrosoftTeamsConfigDAL: Pick; + kmsService: Pick; + microsoftTeamsService: Pick; + }; +}; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index ad5291a13..57a1313c6 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -111,6 +111,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { return; } + // Authentication is handled on a route-level here. + if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) { + return; + } + const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET); if (!authMode) return; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a71a69c20..7e999cfb8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -174,6 +174,9 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal"; +import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; +import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; 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"; @@ -426,6 +429,8 @@ export const registerRoutes = async ( const githubOrgSyncDAL = githubOrgSyncDALFactory(db); const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL); + const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db); + const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -687,6 +692,15 @@ export const registerRoutes = async ( orgDAL, externalGroupOrgRoleMappingDAL }); + + const microsoftTeamsService = microsoftTeamsServiceFactory({ + microsoftTeamsIntegrationDAL, + permissionService, + workflowIntegrationDAL, + kmsService, + serverCfgDAL: superAdminDAL + }); + const superAdminService = superAdminServiceFactory({ userDAL, identityDAL, @@ -700,7 +714,8 @@ export const registerRoutes = async ( orgService, keyStore, licenseService, - kmsService + kmsService, + microsoftTeamsService }); const orgAdminService = orgAdminServiceFactory({ @@ -1026,6 +1041,8 @@ export const registerRoutes = async ( certificateTemplateDAL, projectSlackConfigDAL, slackIntegrationDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsIntegrationDAL, projectTemplateService, groupProjectDAL, smtpService @@ -1150,7 +1167,9 @@ export const registerRoutes = async ( userDAL, licenseService, projectSlackConfigDAL, - resourceMetadataDAL + resourceMetadataDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }); const secretService = secretServiceFactory({ @@ -1212,7 +1231,9 @@ export const registerRoutes = async ( accessApprovalPolicyApproverDAL, projectSlackConfigDAL, kmsService, - groupDAL + groupDAL, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL }); const secretReplicationService = secretReplicationServiceFactory({ @@ -1610,6 +1631,7 @@ export const registerRoutes = async ( await dailyResourceCleanUp.startCleanUp(); await dailyExpiringPkiItemAlert.startSendingAlerts(); await kmsService.startService(); + await microsoftTeamsService.start(); // inject all services server.decorate("services", { @@ -1702,6 +1724,7 @@ export const registerRoutes = async ( kmipOperation: kmipOperationService, gateway: gatewayService, secretRotationV2: secretRotationV2Service, + microsoftTeams: microsoftTeamsService, assumePrivileges: assumePrivilegeService, githubOrgSync: githubOrgSyncConfigService }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6eb1804f1..a55aa2ba4 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -27,7 +27,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { createdAt: true, updatedAt: true, encryptedSlackClientId: true, - encryptedSlackClientSecret: true + encryptedSlackClientSecret: true, + encryptedMicrosoftTeamsAppId: true, + encryptedMicrosoftTeamsClientSecret: true, + encryptedMicrosoftTeamsBotId: true }).extend({ isMigrationModeOn: z.boolean(), defaultAuthOrgSlug: z.string().nullable(), @@ -74,6 +77,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }), slackClientId: z.string().optional(), slackClientSecret: z.string().optional(), + microsoftTeamsAppId: z.string().optional(), + microsoftTeamsClientSecret: z.string().optional(), + microsoftTeamsBotId: z.string().optional(), authConsentContent: z .string() .trim() @@ -197,15 +203,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/integrations/slack/config", + url: "/integrations", config: { rateLimit: readLimit }, schema: { response: { 200: z.object({ - clientId: z.string(), - clientSecret: z.string() + slack: z.object({ + clientId: z.string(), + clientSecret: z.string() + }), + microsoftTeams: z.object({ + appId: z.string(), + clientSecret: z.string(), + botId: z.string() + }) }) } }, @@ -215,9 +228,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async () => { - const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig(); + const adminIntegrationsConfig = await server.services.superAdmin.getAdminIntegrationsConfig(); - return adminSlackConfig; + return adminIntegrationsConfig; } }); diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 50fd33840..d2ba35a7e 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -47,6 +47,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; +import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); @@ -79,6 +80,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { async (workflowIntegrationRouter) => { await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter); await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" }); + await workflowIntegrationRouter.register(registerMicrosoftTeamsRouter, { prefix: "/microsoft-teams" }); }, { prefix: "/workflow-integrations" } ); diff --git a/backend/src/server/routes/v1/microsoft-teams-router.ts b/backend/src/server/routes/v1/microsoft-teams-router.ts new file mode 100644 index 000000000..bf24717d5 --- /dev/null +++ b/backend/src/server/routes/v1/microsoft-teams-router.ts @@ -0,0 +1,381 @@ +import { z } from "zod"; + +import { MicrosoftTeamsIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { WorkflowIntegrationStatus } from "@app/services/workflow-integration/workflow-integration-types"; + +const sanitizedMicrosoftTeamsIntegrationSchema = WorkflowIntegrationsSchema.pick({ + id: true, + description: true, + slug: true, + integration: true +}).merge( + MicrosoftTeamsIntegrationsSchema.pick({ + tenantId: true + }).extend({ + status: z.nativeEnum(WorkflowIntegrationStatus) + }) +); + +export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/client-id", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + clientId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const clientId = await server.services.microsoftTeams.getClientId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return { + clientId + }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + body: z.object({ + redirectUri: z.string(), + tenantId: z.string().uuid(), + slug: z.string(), + description: z.string().optional(), + code: z.string().trim() + }) + }, + + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.microsoftTeams.completeMicrosoftTeamsIntegration({ + tenantId: req.body.tenantId, + slug: req.body.slug, + description: req.body.description, + redirectUri: req.body.redirectUri, + code: req.body.code, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE, + metadata: { + tenantId: req.body.tenantId, + slug: req.body.slug, + description: req.body.description + } + } + }); + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + response: { + 200: sanitizedMicrosoftTeamsIntegrationSchema.array() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const microsoftTeamsIntegrations = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationsByOrg({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST, + metadata: {} + } + }); + + return microsoftTeamsIntegrations; + } + }); + + server.route({ + method: "POST", + url: "/:id/installation-status", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }) + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const microsoftTeamsIntegration = await server.services.microsoftTeams.checkInstallationStatus({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + workflowIntegrationId: req.params.id + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS, + metadata: { + tenantId: microsoftTeamsIntegration.tenantId, + slug: microsoftTeamsIntegration.slug + } + } + }); + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string() + }), + response: { + 200: sanitizedMicrosoftTeamsIntegrationSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const deletedMicrosoftTeamsIntegration = await server.services.microsoftTeams.deleteMicrosoftTeamsIntegration({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE, + metadata: { + tenantId: deletedMicrosoftTeamsIntegration.tenantId, + slug: deletedMicrosoftTeamsIntegration.slug, + id: deletedMicrosoftTeamsIntegration.id + } + } + }); + + return deletedMicrosoftTeamsIntegration; + } + }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string() + }), + response: { + 200: sanitizedMicrosoftTeamsIntegrationSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const microsoftTeamsIntegration = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET, + metadata: { + slug: microsoftTeamsIntegration.slug, + id: microsoftTeamsIntegration.id, + tenantId: microsoftTeamsIntegration.tenantId + } + } + }); + + return microsoftTeamsIntegration; + } + }); + + server.route({ + method: "PATCH", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string() + }), + body: z.object({ + slug: slugSchema({ max: 64 }).optional(), + description: z.string().optional() + }), + response: { + 200: sanitizedMicrosoftTeamsIntegrationSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const microsoftTeamsIntegration = await server.services.microsoftTeams.updateMicrosoftTeamsIntegration({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE, + metadata: { + slug: microsoftTeamsIntegration.slug, + id: microsoftTeamsIntegration.id, + tenantId: microsoftTeamsIntegration.tenantId, + newSlug: req.body.slug, + newDescription: req.body.description + } + } + }); + + return microsoftTeamsIntegration; + } + }); + + server.route({ + method: "GET", + url: "/:workflowIntegrationId/teams", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workflowIntegrationId: z.string() + }), + response: { + 200: z + .object({ + teamId: z.string(), + teamName: z.string(), + channels: z + .object({ + channelName: z.string(), + channelId: z.string() + }) + .array() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const microsoftTeamsIntegration = await server.services.microsoftTeams.getTeams({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + workflowIntegrationId: req.params.workflowIntegrationId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS, + metadata: { + tenantId: microsoftTeamsIntegration.tenantId, + slug: microsoftTeamsIntegration.slug, + id: microsoftTeamsIntegration.id + } + } + }); + + return microsoftTeamsIntegration.teams; + } + }); + + server.route({ + method: "POST", + url: "/message-endpoint", + schema: { + body: z.any(), + response: { + 200: z.any() + } + }, + handler: async (req, res) => { + await server.services.microsoftTeams.handleMessageEndpoint(req, res); + } + }); +}; diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 7df68f39a..fdc729548 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -14,6 +14,7 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; @@ -21,8 +22,10 @@ import { re2Validator } from "@app/lib/zod"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns"; import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types"; import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators"; +import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types"; import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas"; import { sanitizedServiceTokenSchema } from "../v2/service-token-router"; @@ -740,55 +743,112 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:workspaceId/slack-config", + url: "/:workspaceId/workflow-integration-config/:integration", config: { rateLimit: readLimit }, schema: { params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim(), + integration: z.nativeEnum(WorkflowIntegration) }), response: { - 200: ProjectSlackConfigsSchema.pick({ - id: true, - slackIntegrationId: true, - isAccessRequestNotificationEnabled: true, - accessRequestChannels: true, - isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + 200: z.discriminatedUnion("integration", [ + ProjectSlackConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + accessRequestChannels: true, + isSecretRequestNotificationEnabled: true, + secretRequestChannels: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.SLACK), + integrationId: z.string() + }) + ), + ProjectMicrosoftTeamsConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + accessRequestChannels: true, + isSecretRequestNotificationEnabled: true, + secretRequestChannels: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), + integrationId: z.string() + }) + ) + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const config = await server.services.project.getProjectWorkflowIntegrationConfig({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + integration: req.params.integration + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG, + metadata: { + id: config.id, + integration: config.integration + } + } + }); + + return config; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectId/workflow-integration/:integration/:integrationId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim(), + integration: z.nativeEnum(WorkflowIntegration), + integrationId: z.string() + }), + response: { + 200: z.object({ + integrationConfig: z.object({ + id: z.string() + }) }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const slackConfig = await server.services.project.getProjectSlackConfig({ + const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({ actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + projectId: req.params.projectId, + integration: req.params.integration, + integrationId: req.params.integrationId }); - if (slackConfig) { - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: req.params.workspaceId, - event: { - type: EventType.GET_PROJECT_SLACK_CONFIG, - metadata: { - id: slackConfig.id - } - } - }); - } - - return slackConfig; + return { + integrationConfig: deletedIntegration + }; } }); server.route({ method: "PUT", - url: "/:workspaceId/slack-config", + url: "/:workspaceId/workflow-integration", config: { rateLimit: readLimit }, @@ -796,27 +856,57 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { params: z.object({ workspaceId: z.string().trim() }), - body: z.object({ - slackIntegrationId: z.string(), - isAccessRequestNotificationEnabled: z.boolean(), - accessRequestChannels: validateSlackChannelsField, - isSecretRequestNotificationEnabled: z.boolean(), - secretRequestChannels: validateSlackChannelsField - }), - response: { - 200: ProjectSlackConfigsSchema.pick({ - id: true, - slackIntegrationId: true, - isAccessRequestNotificationEnabled: true, - accessRequestChannels: true, - isSecretRequestNotificationEnabled: true, - secretRequestChannels: true + + body: z.discriminatedUnion("integration", [ + z.object({ + integration: z.literal(WorkflowIntegration.SLACK), + integrationId: z.string(), + accessRequestChannels: validateSlackChannelsField, + secretRequestChannels: validateSlackChannelsField, + isAccessRequestNotificationEnabled: z.boolean(), + isSecretRequestNotificationEnabled: z.boolean() + }), + z.object({ + integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), + integrationId: z.string(), + accessRequestChannels: validateMicrosoftTeamsChannelsSchema, + secretRequestChannels: validateMicrosoftTeamsChannelsSchema, + isAccessRequestNotificationEnabled: z.boolean(), + isSecretRequestNotificationEnabled: z.boolean() }) + ]), + response: { + 200: z.discriminatedUnion("integration", [ + ProjectSlackConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + accessRequestChannels: true, + isSecretRequestNotificationEnabled: true, + secretRequestChannels: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.SLACK), + integrationId: z.string() + }) + ), + ProjectMicrosoftTeamsConfigsSchema.pick({ + id: true, + isAccessRequestNotificationEnabled: true, + isSecretRequestNotificationEnabled: true + }).merge( + z.object({ + integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS), + integrationId: z.string(), + accessRequestChannels: validateMicrosoftTeamsChannelsSchema, + secretRequestChannels: validateMicrosoftTeamsChannelsSchema + }) + ) + ]) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const slackConfig = await server.services.project.updateProjectSlackConfig({ + const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({ actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actor: req.permission.type, @@ -829,19 +919,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { ...req.auditLogInfo, projectId: req.params.workspaceId, event: { - type: EventType.UPDATE_PROJECT_SLACK_CONFIG, + type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG, metadata: { - id: slackConfig.id, - slackIntegrationId: slackConfig.slackIntegrationId, - isAccessRequestNotificationEnabled: slackConfig.isAccessRequestNotificationEnabled, - accessRequestChannels: slackConfig.accessRequestChannels, - isSecretRequestNotificationEnabled: slackConfig.isSecretRequestNotificationEnabled, - secretRequestChannels: slackConfig.secretRequestChannels + id: workflowIntegrationConfig.id, + integrationId: workflowIntegrationConfig.integrationId, + integration: workflowIntegrationConfig.integration, + isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled, + accessRequestChannels: workflowIntegrationConfig.accessRequestChannels, + isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled, + secretRequestChannels: workflowIntegrationConfig.secretRequestChannels } } }); - return slackConfig; + return workflowIntegrationConfig; } }); diff --git a/backend/src/server/routes/v1/workflow-integration-router.ts b/backend/src/server/routes/v1/workflow-integration-router.ts index 839d7b056..937a43843 100644 --- a/backend/src/server/routes/v1/workflow-integration-router.ts +++ b/backend/src/server/routes/v1/workflow-integration-router.ts @@ -7,7 +7,8 @@ const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({ id: true, description: true, slug: true, - integration: true + integration: true, + status: true }); export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts new file mode 100644 index 000000000..4111115bf --- /dev/null +++ b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts @@ -0,0 +1,706 @@ +/* eslint-disable class-methods-use-this */ +import axios from "axios"; +import { TeamsActivityHandler, TurnContext } from "botbuilder"; +import jwt from "jsonwebtoken"; +import { Knex } from "knex"; +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types"; + +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal"; +import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; +import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal"; + +const ConsentError = "AADSTS65001"; + +export const verifyTenantFromCode = async ( + tenantId: string, + code: string, + redirectUri: string, + clientId: string, + clientSecret: string +) => { + const getAccessToken = async (params: URLSearchParams) => { + const response = await axios + .post<{ access_token: string }>(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, params, { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }) + .catch((err) => { + if (axios.isAxiosError(err)) { + if ((err.response?.data as { error_description?: string })?.error_description?.includes(ConsentError)) { + throw new BadRequestError({ + message: "Unable to verify tenant, please ensure that you have granted admin consent." + }); + } + logger.error(err.response?.data, "Error fetching Microsoft Teams access token"); + } + throw err; + }); + + return response.data.access_token; + }; + + // Azure App-based auth + const applicationAccessToken = await getAccessToken( + new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + scope: "https://graph.microsoft.com/.default", + redirect_uri: redirectUri, + grant_type: "client_credentials" + }) + ); + + // User-based auth + const authorizationAccessToken = await getAccessToken( + new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + scope: "https://graph.microsoft.com/.default", + redirect_uri: redirectUri, + grant_type: "authorization_code", + code + }) + ); + + // Verify application token + const { tid: tenantIdFromApplicationAccessToken } = jwt.decode(applicationAccessToken) as { tid: string }; + + if (tenantIdFromApplicationAccessToken !== tenantId) { + throw new BadRequestError({ + message: `Invalid application token tenant ID. Expected ${tenantId}, got ${tenantIdFromApplicationAccessToken}` + }); + } + + // Verify user authorization token + const { tid: tenantIdFromAuthorizationAccessToken } = jwt.decode(authorizationAccessToken) as { tid: string }; + + if (tenantIdFromAuthorizationAccessToken !== tenantId) { + throw new BadRequestError({ + message: `Invalid authorization token tenant ID. Expected ${tenantId}, got ${tenantIdFromAuthorizationAccessToken}` + }); + } +}; + +export const getMicrosoftTeamsAccessToken = async ( + { + orgId, + microsoftTeamsIntegrationId, + tenantId, + clientId, + clientSecret, + kmsService, + microsoftTeamsIntegrationDAL, + getBotFrameworkToken + }: { + microsoftTeamsIntegrationId: string; + orgId: string; + tenantId: string; + clientId: string; + clientSecret: string; + kmsService: Pick; + microsoftTeamsIntegrationDAL: Pick; + getBotFrameworkToken?: boolean; + }, + tx?: Knex +) => { + try { + const details = getBotFrameworkToken + ? { + uri: "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token", + scope: "https://api.botframework.com/.default" + } + : { + uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, + scope: "https://graph.microsoft.com/.default" + }; + + const integration = await microsoftTeamsIntegrationDAL.findOne( + { + id: microsoftTeamsIntegrationId + }, + tx + ); + + if (!integration) { + throw new BadRequestError({ message: "Microsoft Teams integration not found" }); + } + + if (getBotFrameworkToken) { + // If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one. + const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000); + + if ( + integration.encryptedBotAccessToken && + integration.botAccessTokenExpiresAt && + integration.botAccessTokenExpiresAt > currentTime + ) { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + orgId, + type: KmsDataKey.Organization + }); + + const botAccessToken = decryptor({ + cipherTextBlob: integration.encryptedBotAccessToken + }); + + return botAccessToken.toString(); + } + } else { + // If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one. + const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000); + + if ( + integration.encryptedAccessToken && + integration.accessTokenExpiresAt && + integration.accessTokenExpiresAt > currentTime + ) { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + orgId, + type: KmsDataKey.Organization + }); + + const accessToken = decryptor({ + cipherTextBlob: integration.encryptedAccessToken + }); + + return accessToken.toString(); + } + } + + const tokenResponse = await axios.post<{ access_token: string; expires_in: number }>( + details.uri, + new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + scope: details.scope, + grant_type: "client_credentials" + }) + ); + + if (getBotFrameworkToken) { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + orgId, + type: KmsDataKey.Organization + }); + + const { cipherTextBlob: encryptedBotAccessToken } = encryptor({ + plainText: Buffer.from(tokenResponse.data.access_token) + }); + + const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000); + + await microsoftTeamsIntegrationDAL.update( + { + id: microsoftTeamsIntegrationId + }, + { + botAccessTokenExpiresAt: expiresAt, + encryptedBotAccessToken + }, + tx + ); + } else { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + orgId, + type: KmsDataKey.Organization + }); + + const { cipherTextBlob: encryptedAccessToken } = encryptor({ + plainText: Buffer.from(tokenResponse.data.access_token) + }); + + const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000); + + await microsoftTeamsIntegrationDAL.update( + { + id: microsoftTeamsIntegrationId + }, + { + accessTokenExpiresAt: expiresAt, + encryptedAccessToken + }, + tx + ); + } + + return tokenResponse.data.access_token; + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error( + error.response?.data, + `getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token [status-code=${error.response?.status}]` + ); + } else { + logger.error(error, "getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token"); + } + throw error; + } +}; + +export const isBotInstalledInTenant = async ( + { + tenantId, + botAppId, + botAppPassword, + botId, + orgId, + kmsService, + microsoftTeamsIntegrationDAL, + microsoftTeamsIntegrationId + }: { + tenantId: string; + botAppId: string; + botAppPassword: string; + botId: string; + orgId: string; + kmsService: Pick; + microsoftTeamsIntegrationDAL: Pick; + microsoftTeamsIntegrationId: string; + }, + tx?: Knex +) => { + try { + const botAccessToken = await getMicrosoftTeamsAccessToken( + { + tenantId, + clientId: botAppId.toString(), + clientSecret: botAppPassword.toString(), + getBotFrameworkToken: true, + orgId, + kmsService, + microsoftTeamsIntegrationDAL, + microsoftTeamsIntegrationId + }, + tx + ).catch(() => null); + + const accessToken = await getMicrosoftTeamsAccessToken( + { + orgId, + tenantId, + clientId: botAppId.toString(), + clientSecret: botAppPassword.toString(), + kmsService, + microsoftTeamsIntegrationDAL, + microsoftTeamsIntegrationId + }, + tx + ).catch(() => null); + + if (!botAccessToken || !accessToken) { + return { + accessToken: null, + botAccessToken: null, + installed: false, + internalId: null + } as const; + } + + const appsResponse = await axios + .get<{ value: { id: string; displayName: string; distributionMethod: string; externalId: string }[] }>( + "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps", + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ) + .catch((error) => { + logger.error(error, "Error fetching installed apps"); + return null; + }); + + if (!appsResponse) { + return { + installed: false, + internalId: null, + accessToken, + botAccessToken + } as const; + } + + const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId); + + if (!botInstalledInTenant) { + return { + installed: false, + internalId: null, + accessToken, + botAccessToken + } as const; + } + + return { + installed: true, + internalId: botInstalledInTenant.id, + accessToken, + botAccessToken + } as const; + } catch (error) { + logger.error(error, "Error fetching installed apps"); + return { + installed: false, + internalId: null, + accessToken: null, + botAccessToken: null + } as const; + } +}; + +export const buildTeamsPayload = (notification: TNotification) => { + const appCfg = getConfig(); + + switch (notification.type) { + case TriggerFeature.SECRET_APPROVAL: { + const { payload } = notification; + + const adaptiveCard = { + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.5", + body: [ + { + type: "TextBlock", + text: "Secret approval request", + weight: "Bolder", + size: "Large" + }, + { + type: "TextBlock", + text: `A secret approval request has been opened by ${payload.userEmail}.`, + wrap: true + }, + { + type: "FactSet", + facts: [ + { + title: "Environment", + value: payload.environment + }, + { + title: "Secret path", + value: payload.secretPath || "/" + }, + { + title: `Secret Key${payload.secretKeys.length > 1 ? "s" : ""}`, + value: payload.secretKeys.join(", ") + } + ] + } + ], + actions: [ + { + type: "Action.OpenUrl", + title: "View request in Infisical", + url: `${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${payload.requestId}` + } + ] + }; + + return { + adaptiveCard + }; + } + + case TriggerFeature.ACCESS_REQUEST: { + const { payload } = notification; + + const adaptiveCard = { + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.5", + body: [ + { + type: "TextBlock", + text: "New access approval request pending for review", + weight: "Bolder", + size: "Large" + }, + { + type: "TextBlock", + text: `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${ + payload.isTemporary ? "temporary" : "permanent" + } access to path '${payload.secretPath}' in the ${payload.environment} environment of ${ + payload.projectName + } project.`, + wrap: true + }, + { + type: "TextBlock", + text: `The following permissions are requested: ${payload.permissions.join(", ")}`, + wrap: true + }, + payload.note + ? { + type: "TextBlock", + text: `**User Note**: ${payload.note}`, + wrap: true + } + : null + ].filter(Boolean), + actions: [ + { + type: "Action.OpenUrl", + title: "View request in Infisical", + url: payload.approvalUrl + } + ] + }; + + return { + adaptiveCard + }; + } + + default: { + throw new BadRequestError({ + message: "Teams notification type not supported." + }); + } + } +}; + +export class TeamsBot extends TeamsActivityHandler { + private botAppId: string; + + private botAppPassword: string; + + private workflowIntegrationDAL: Pick; + + private microsoftTeamsIntegrationDAL: Pick; + + constructor({ + botAppId, + botAppPassword, + workflowIntegrationDAL, + microsoftTeamsIntegrationDAL + }: { + botAppId: string; + botAppPassword: string; + workflowIntegrationDAL: Pick; + microsoftTeamsIntegrationDAL: Pick; + }) { + super(); + + this.botAppId = botAppId; + this.botAppPassword = botAppPassword; + this.workflowIntegrationDAL = workflowIntegrationDAL; + this.microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDAL; + + // We know when a bot is added, but we can't know when it's fully removed from the tenant. + this.onTeamsMembersAddedEvent(async (membersAdded, _, context) => { + const botWasAdded = membersAdded.some((member) => member.id === context.activity.recipient.id); + + if (botWasAdded && context.activity.conversation.tenantId) { + const microsoftTeamIntegration = await this.microsoftTeamsIntegrationDAL + .findOne({ + tenantId: context.activity.conversation.tenantId + }) + .catch(() => null); + + if (microsoftTeamIntegration) { + await this.workflowIntegrationDAL + .update( + { + id: microsoftTeamIntegration.id, + status: WorkflowIntegrationStatus.PENDING + }, + { + status: WorkflowIntegrationStatus.INSTALLED + } + ) + .catch((error) => { + logger.error(error, "Microsoft Teams Workflow Integration: Failed to update workflow integration"); + }); + } + + // This is required in order for the bot to send proactive messages, which is required for the bot to pass the bot release validation step. + await context.sendActivity( + "👋 Thanks for installing the Infisical app! You can now use the bot to send notifications to your selected teams." + ); + } + }); + } + + async run(context: TurnContext) { + logger.info(context, "Processing Microsoft Teams context"); + await super.run(context); + } + + async sendMessageToChannel( + botAccessToken: string, + tenantId: string, + channelId: string, + teamId: string, + notification: TNotification + ) { + try { + const { adaptiveCard } = buildTeamsPayload(notification); + + const adaptiveCardActivity = { + type: "message", + attachments: [ + { + contentType: "application/vnd.microsoft.card.adaptive", + content: adaptiveCard + } + ], + conversation: { + id: channelId, + isGroup: true + }, + channelData: { + channel: { + id: channelId + }, + team: { + id: teamId + } + } + }; + + await axios.post( + `https://smba.trafficmanager.net/amer/v3/conversations/${channelId}/activities`, + adaptiveCardActivity, + { + headers: { + Authorization: `Bearer ${botAccessToken}`, + "Content-Type": "application/json" + } + } + ); + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error( + error.response?.data, + `sendMessageToChannel: Axios Error, Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]` + ); + } else { + logger.error( + error, + `sendMessageToChannel: Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]` + ); + } + throw error; + } + } + + async getTeamsAndChannels(accessToken: string, internalAppId: string) { + try { + let teamsNextLink: string = "https://graph.microsoft.com/v1.0/teams"; + + let allTeams: { displayName: string; id: string }[] = []; + while (teamsNextLink?.length) { + try { + // eslint-disable-next-line no-await-in-loop + const response = await axios.get<{ + value: { displayName: string; id: string }[]; + "@odata.nextLink"?: string; + }>(teamsNextLink, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + allTeams = allTeams.concat(response.data.value); + teamsNextLink = response.data["@odata.nextLink"] || ""; + } catch (error) { + logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch teams"); + throw error; + } + } + + const result = []; + + for await (const team of allTeams) { + try { + // Get installed apps for this team + const installedAppsResponse = await axios.get<{ value: { teamsAppDefinition: { teamsAppId: string } }[] }>( + `https://graph.microsoft.com/v1.0/teams/${team.id}/installedApps?$expand=teamsAppDefinition`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + if (!installedAppsResponse.data.value.some((app) => app.teamsAppDefinition.teamsAppId === internalAppId)) { + // eslint-disable-next-line no-continue + continue; + } + } catch (error) { + // eslint-disable-next-line no-continue + continue; // skip this team if we can't determine if the bot is installed + } + + let allChannels: { displayName: string; id: string }[] = []; + + let channelNextLink: string = `https://graph.microsoft.com/v1.0/teams/${team.id}/channels`; + + while (channelNextLink?.length) { + // eslint-disable-next-line no-await-in-loop + const resp = await axios + .get<{ + value: { displayName: string; id: string }[]; + "@odata.nextLink"?: string; + }>(channelNextLink, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + .catch((error) => { + if (axios.isAxiosError(error)) { + logger.error( + error.response?.data, + "getTeamsAndChannels: Axios error, Microsoft Teams Workflow Integration: Failed to fetch channels" + ); + } else { + logger.error( + error, + "getTeamsAndChannels: Microsoft Teams Workflow Integration: Failed to fetch channels" + ); + } + throw error; + }); + + allChannels = allChannels.concat(resp.data.value); + channelNextLink = resp.data["@odata.nextLink"] || ""; + } + + const channels = allChannels.map((channel) => ({ + channelName: channel.displayName, + channelId: channel.id + })); + + result.push({ + teamId: team.id, + teamName: team.displayName, + channels + }); + } + + return result; + } catch (error) { + logger.error(error, "Microsoft Teams Workflow Integration: Error fetching teams and channels"); + throw error; + } + } +} + +export const validateMicrosoftTeamsChannelsSchema = z + .object({ + teamId: z.string(), + channelIds: z.array(z.string()).min(1) + }) + .optional() + .refine((data) => data === undefined || data?.channelIds.length <= 20, { + message: "You can only select up to 20 Microsoft Teams channels" + }); diff --git a/backend/src/services/microsoft-teams/microsoft-teams-integration-dal.ts b/backend/src/services/microsoft-teams/microsoft-teams-integration-dal.ts new file mode 100644 index 000000000..7935d60cc --- /dev/null +++ b/backend/src/services/microsoft-teams/microsoft-teams-integration-dal.ts @@ -0,0 +1,62 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TMicrosoftTeamsIntegrations, TWorkflowIntegrations } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TMicrosoftTeamsIntegrationDALFactory = ReturnType; + +export const microsoftTeamsIntegrationDALFactory = (db: TDbClient) => { + const microsoftTeamsIntegrationOrm = ormify(db, TableName.MicrosoftTeamsIntegrations); + + const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => { + try { + return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations) + .join( + TableName.WorkflowIntegrations, + `${TableName.MicrosoftTeamsIntegrations}.id`, + `${TableName.WorkflowIntegrations}.id` + ) + .select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations)) + .select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("description").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("integration").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("slug").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("status").withSchema(TableName.WorkflowIntegrations)) + .where(`${TableName.WorkflowIntegrations}.id`, id) + .first(); + } catch (error) { + throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" }); + } + }; + + const findWithWorkflowIntegrationDetails = async ( + filter: Partial & Partial, + tx?: Knex + ) => { + try { + return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations) + .join( + TableName.WorkflowIntegrations, + `${TableName.MicrosoftTeamsIntegrations}.id`, + `${TableName.WorkflowIntegrations}.id` + ) + .select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations)) + .select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("description").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("integration").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("slug").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("status").withSchema(TableName.WorkflowIntegrations)) + .where(filter); + } catch (error) { + throw new DatabaseError({ error, name: "Find with Workflow integration details" }); + } + }; + + return { + ...microsoftTeamsIntegrationOrm, + findByIdWithWorkflowIntegrationDetails, + findWithWorkflowIntegrationDetails + }; +}; diff --git a/backend/src/services/microsoft-teams/microsoft-teams-service.ts b/backend/src/services/microsoft-teams/microsoft-teams-service.ts new file mode 100644 index 000000000..3712a0793 --- /dev/null +++ b/backend/src/services/microsoft-teams/microsoft-teams-service.ts @@ -0,0 +1,710 @@ +import { ForbiddenError } from "@casl/ability"; +import { + CloudAdapter, + ConfigurationBotFrameworkAuthentication, + ConfigurationServiceClientCredentialFactory, + Request, + Response +} from "botbuilder"; +import { FastifyReply, FastifyRequest } from "fastify"; + +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; + +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal"; +import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal"; +import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; +import { + getMicrosoftTeamsAccessToken, + isBotInstalledInTenant, + TeamsBot, + verifyTenantFromCode +} from "./microsoft-teams-fns"; +import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal"; +import { + TCheckInstallationStatusDTO, + TCreateMicrosoftTeamsIntegrationDTO, + TDeleteMicrosoftTeamsIntegrationDTO, + TGetClientIdDTO, + TGetMicrosoftTeamsIntegrationByIdDTO, + TGetMicrosoftTeamsIntegrationByOrgDTO, + TGetTeamsDTO, + TSendNotificationDTO, + TUpdateMicrosoftTeamsIntegrationDTO +} from "./microsoft-teams-types"; + +function requestBodyToRecord(body: unknown): Record { + // if body is null or undefined, return an empty object + if (body === null || body === undefined) { + return {}; + } + + // if body is not an object or is an array, return an empty object + if (typeof body !== "object" || Array.isArray(body)) { + return {}; + } + + // at this point, we know body is an object, so safe to cast + return body as Record; +} + +type TMicrosoftTeamsServiceFactoryDep = { + microsoftTeamsIntegrationDAL: Pick< + TMicrosoftTeamsIntegrationDALFactory, + | "deleteById" + | "updateById" + | "create" + | "findOne" + | "findById" + | "findByIdWithWorkflowIntegrationDetails" + | "findWithWorkflowIntegrationDetails" + | "update" + >; + permissionService: Pick; + kmsService: Pick; + workflowIntegrationDAL: Pick< + TWorkflowIntegrationDALFactory, + "transaction" | "create" | "updateById" | "deleteById" | "update" | "findOne" + >; + serverCfgDAL: Pick; +}; + +export type TMicrosoftTeamsServiceFactory = ReturnType; + +const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; + +export const microsoftTeamsServiceFactory = ({ + permissionService, + serverCfgDAL, + kmsService, + microsoftTeamsIntegrationDAL, + workflowIntegrationDAL +}: TMicrosoftTeamsServiceFactoryDep) => { + let teamsBot: TeamsBot | null = null; + let adapter: CloudAdapter | null = null; + + const initializeTeamsBot = async ({ botAppId, botAppPassword }: { botAppId: string; botAppPassword: string }) => { + logger.info("Initializing Microsoft Teams bot"); + teamsBot = new TeamsBot({ + botAppId, + botAppPassword, + workflowIntegrationDAL, + microsoftTeamsIntegrationDAL + }); + + adapter = new CloudAdapter( + new ConfigurationBotFrameworkAuthentication( + {}, + new ConfigurationServiceClientCredentialFactory({ + MicrosoftAppId: botAppId, + MicrosoftAppPassword: botAppPassword, + MicrosoftAppType: "MultiTenant" + }) + ) + ); + }; + + const start = async () => { + try { + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + + if ( + serverCfg?.encryptedMicrosoftTeamsAppId && + serverCfg?.encryptedMicrosoftTeamsClientSecret && + serverCfg?.encryptedMicrosoftTeamsBotId + ) { + const decryptWithRoot = kmsService.decryptWithRootKey(); + const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId); + const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret); + + await initializeTeamsBot({ + botAppId: decryptedAppId.toString(), + botAppPassword: decryptedAppPassword.toString() + }); + } + } catch (err) { + logger.error(err, "Error initializing Microsoft Teams bot on startup"); + } + }; + + const checkInstallationStatus = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + workflowIntegrationId + }: TCheckInstallationStatusDTO) => { + const microsoftTeamsIntegration = + await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId); + + if (!microsoftTeamsIntegration) { + throw new NotFoundError({ + message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found` + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + microsoftTeamsIntegration.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (!serverCfg) { + throw new BadRequestError({ + message: "Failed to get server configuration." + }); + } + + if ( + !serverCfg.encryptedMicrosoftTeamsAppId || + !serverCfg.encryptedMicrosoftTeamsClientSecret || + !serverCfg.encryptedMicrosoftTeamsBotId + ) { + throw new BadRequestError({ + message: "Microsoft Teams app ID, client secret, or bot ID is not set" + }); + } + const decryptWithRoot = kmsService.decryptWithRootKey(); + const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId); + const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret); + const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId); + + const teamsBotInfo = await isBotInstalledInTenant({ + tenantId: microsoftTeamsIntegration.tenantId, + botAppId: decryptedAppId.toString(), + botAppPassword: decryptedAppPassword.toString(), + botId: decryptedBotId.toString(), + orgId: microsoftTeamsIntegration.orgId, + kmsService, + microsoftTeamsIntegrationDAL, + microsoftTeamsIntegrationId: microsoftTeamsIntegration.id + }); + + if (!teamsBotInfo.installed) { + if (microsoftTeamsIntegration.status === WorkflowIntegrationStatus.INSTALLED) { + await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, { + status: WorkflowIntegrationStatus.PENDING + }); + } + + throw new BadRequestError({ + message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant" + }); + } + + if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) { + await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, { + status: WorkflowIntegrationStatus.INSTALLED + }); + } + + return microsoftTeamsIntegration; + }; + + const completeMicrosoftTeamsIntegration = async ({ + code, + actor, + actorId, + actorOrgId, + actorAuthMethod, + tenantId, + slug, + description, + redirectUri + }: TCreateMicrosoftTeamsIntegrationDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); + + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (!serverCfg) { + throw new BadRequestError({ + message: "Failed to get server configuration." + }); + } + + const { encryptedMicrosoftTeamsAppId, encryptedMicrosoftTeamsClientSecret, encryptedMicrosoftTeamsBotId } = + serverCfg; + + if (!encryptedMicrosoftTeamsAppId || !encryptedMicrosoftTeamsClientSecret || !encryptedMicrosoftTeamsBotId) { + throw new BadRequestError({ + message: "Microsoft Teams app ID, client secret, or bot ID is not set" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId); + const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret); + const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId); + + await verifyTenantFromCode(tenantId, code, redirectUri, botAppId.toString(), botAppPassword.toString()); + + await workflowIntegrationDAL.transaction(async (tx) => { + const workflowIntegration = await workflowIntegrationDAL.create( + { + description, + orgId: actorOrgId, + slug, + integration: WorkflowIntegration.MICROSOFT_TEAMS, + status: WorkflowIntegrationStatus.PENDING + }, + tx + ); + + const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL + .create( + { + // @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID + id: workflowIntegration.id, + tenantId + }, + tx + ) + .catch((err) => { + if (err instanceof DatabaseError) { + if ((err.error as Error)?.stack?.includes("duplicate key value violates unique constraint")) + throw new BadRequestError({ + message: "Microsoft Teams integration with the same Tenant ID already exists." + }); + } + throw err; + }); + + const teamsBotInfo = await isBotInstalledInTenant( + { + tenantId: microsoftTeamsIntegration.tenantId, + botAppId: botAppId.toString(), + botAppPassword: botAppPassword.toString(), + botId: botId.toString(), + orgId: workflowIntegration.orgId, + kmsService, + microsoftTeamsIntegrationDAL, + microsoftTeamsIntegrationId: microsoftTeamsIntegration.id + }, + tx + ); + + if (teamsBotInfo.installed) { + const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({ + orgId: workflowIntegration.orgId, + type: KmsDataKey.Organization + }); + const { cipherTextBlob: encryptedAccessToken } = orgDataKeyEncryptor({ + plainText: Buffer.from(teamsBotInfo.accessToken, "utf8") + }); + + const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({ + plainText: Buffer.from(teamsBotInfo.botAccessToken, "utf8") + }); + await microsoftTeamsIntegrationDAL.updateById( + microsoftTeamsIntegration.id, + { + internalTeamsAppId: teamsBotInfo.internalId, + encryptedAccessToken, + encryptedBotAccessToken + }, + tx + ); + + await workflowIntegrationDAL.updateById( + workflowIntegration.id, + { + status: WorkflowIntegrationStatus.INSTALLED + }, + tx + ); + } + }); + }; + const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (!serverCfg) { + throw new BadRequestError({ + message: "Failed to get server configuration." + }); + } + + if (!serverCfg.encryptedMicrosoftTeamsAppId) { + throw new BadRequestError({ + message: "Microsoft Teams app ID is not set" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const clientId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId); + + return clientId.toString(); + }; + const getMicrosoftTeamsIntegrationsByOrg = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod + }: TGetMicrosoftTeamsIntegrationByOrgDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); + + const microsoftTeamsIntegrations = await microsoftTeamsIntegrationDAL.findWithWorkflowIntegrationDetails({ + orgId: actorOrgId, + status: WorkflowIntegrationStatus.INSTALLED + }); + + return microsoftTeamsIntegrations.map((integration) => ({ + ...integration, + status: integration.status as WorkflowIntegrationStatus, + tenantId: integration.tenantId + })); + }; + + const getMicrosoftTeamsIntegrationById = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TGetMicrosoftTeamsIntegrationByIdDTO) => { + const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); + if (!microsoftTeamsIntegration) { + throw new NotFoundError({ + message: "Microsoft Teams integration not found." + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + microsoftTeamsIntegration.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + + return { + ...microsoftTeamsIntegration, + status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus + }; + }; + + const updateMicrosoftTeamsIntegration = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + slug, + description + }: TUpdateMicrosoftTeamsIntegrationDTO) => { + const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); + if (!microsoftTeamsIntegration) { + throw new NotFoundError({ + message: `Microsoft Teams integration with ID ${id} not found` + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + microsoftTeamsIntegration.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const updatedIntegration = await workflowIntegrationDAL.transaction(async (tx) => { + await workflowIntegrationDAL.updateById( + microsoftTeamsIntegration.id, + { + slug, + description + }, + tx + ); + + const integration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails( + microsoftTeamsIntegration.id, + tx + ); + + if (!integration) { + throw new NotFoundError({ + message: `Microsoft Teams integration with ID ${microsoftTeamsIntegration.id} not found` + }); + } + + return { + ...integration, + status: integration.status as WorkflowIntegrationStatus + }; + }); + + return updatedIntegration; + }; + + const deleteMicrosoftTeamsIntegration = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TDeleteMicrosoftTeamsIntegrationDTO) => { + const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); + if (!microsoftTeamsIntegration) { + throw new NotFoundError({ + message: `Microsoft Teams integration with ID ${id} not found` + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + microsoftTeamsIntegration.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); + + await workflowIntegrationDAL.deleteById(id); + + return { + ...microsoftTeamsIntegration, + status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus + }; + }; + + const getTeams = async ({ actorId, actor, actorOrgId, actorAuthMethod, workflowIntegrationId }: TGetTeamsDTO) => { + const microsoftTeamsIntegration = + await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId); + + if (!microsoftTeamsIntegration) { + throw new NotFoundError({ + message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found` + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + microsoftTeamsIntegration.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + + if (!teamsBot || !adapter) { + throw new BadRequestError({ + message: "Unable to get teams and channels because the Microsoft Teams bot is uninitialized" + }); + } + + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (!serverCfg) { + throw new BadRequestError({ + message: "Failed to get server configuration." + }); + } + + if ( + !serverCfg.encryptedMicrosoftTeamsAppId || + !serverCfg.encryptedMicrosoftTeamsClientSecret || + !serverCfg.encryptedMicrosoftTeamsBotId + ) { + throw new BadRequestError({ + message: "Microsoft Teams app ID, client secret, or bot ID is not set" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId); + const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret); + const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId); + + const { installed, internalId, accessToken } = await isBotInstalledInTenant({ + tenantId: microsoftTeamsIntegration.tenantId, + botAppId: decryptedAppId.toString(), + botAppPassword: decryptedAppPassword.toString(), + botId: decryptedBotId.toString(), + orgId: microsoftTeamsIntegration.orgId, + kmsService, + microsoftTeamsIntegrationDAL, + microsoftTeamsIntegrationId: microsoftTeamsIntegration.id + }); + + if (!installed) { + throw new BadRequestError({ + message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant" + }); + } + + const teams = await teamsBot.getTeamsAndChannels(accessToken, internalId); + + return { + ...microsoftTeamsIntegration, + teams + }; + }; + + const handleMessageEndpoint = async (req: FastifyRequest, res: FastifyReply) => { + if (!teamsBot || !adapter) { + throw new BadRequestError({ + message: "Unable to handle message endpoint because the Microsoft Teams bot is uninitialized" + }); + } + + // We need to manually build a Response object because the BotFrameworkAdapter expects a Response object. We are using FastifyReply as the underlying socket. + const response: Response = { + socket: res.raw.socket, + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + end(...args: any[]): unknown { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + res.raw.end(...args); + return this; + }, + + header(name: string, value: unknown): unknown { + res.raw.setHeader(name, value as string); + return this; + }, + + send(...args: unknown[]): unknown { + // For the first argument, which is typically the body + if (args.length > 0) { + const body = args[0]; + + if (typeof body === "string" || Buffer.isBuffer(body)) { + res.raw.write(body); + } else if (body !== null && body !== undefined) { + const json = JSON.stringify(body); + if (!res.raw.headersSent && !res.raw.getHeader("content-type")) { + res.raw.setHeader("content-type", "application/json"); + } + res.raw.write(json); + } + } + + const lastArg = args[args.length - 1]; + if (typeof lastArg === "function") { + lastArg(); + } + + return this; + }, + + status(code: number): unknown { + res.raw.statusCode = code; + return this; + } + }; + + const request: Request = { + body: requestBodyToRecord(req.body), + headers: req.headers, + method: req.method + }; + + await adapter.process(request, response, async (context) => { + await teamsBot?.run(context); + }); + }; + + const sendNotification = async ({ + tenantId, + target, + notification, + orgId, + microsoftTeamsIntegrationId + }: TSendNotificationDTO) => { + if (!teamsBot || !adapter) { + throw new BadRequestError({ + message: "Unable to send notification because the Microsoft Teams bot is uninitialized" + }); + } + + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (!serverCfg) { + throw new BadRequestError({ + message: "Failed to get server configuration." + }); + } + + if ( + !serverCfg.encryptedMicrosoftTeamsAppId || + !serverCfg.encryptedMicrosoftTeamsClientSecret || + !serverCfg.encryptedMicrosoftTeamsBotId + ) { + throw new BadRequestError({ + message: "Microsoft Teams app ID, client secret, or bot ID is not set" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const botAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId); + const botAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret); + + const botAccessToken = await getMicrosoftTeamsAccessToken({ + tenantId, + clientId: botAppId.toString(), + clientSecret: botAppPassword.toString(), + getBotFrameworkToken: true, + orgId, + kmsService, + microsoftTeamsIntegrationDAL, + microsoftTeamsIntegrationId + }); + + for await (const channelId of target.channelIds) { + await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, notification); + } + }; + + return { + getMicrosoftTeamsIntegrationsByOrg, + getMicrosoftTeamsIntegrationById, + updateMicrosoftTeamsIntegration, + deleteMicrosoftTeamsIntegration, + completeMicrosoftTeamsIntegration, + initializeTeamsBot, + getTeams, + handleMessageEndpoint, + start, + sendNotification, + checkInstallationStatus, + getClientId + }; +}; diff --git a/backend/src/services/microsoft-teams/microsoft-teams-types.ts b/backend/src/services/microsoft-teams/microsoft-teams-types.ts new file mode 100644 index 000000000..7427cb0d5 --- /dev/null +++ b/backend/src/services/microsoft-teams/microsoft-teams-types.ts @@ -0,0 +1,42 @@ +import { TOrgPermission } from "@app/lib/types"; +import { TNotification } from "@app/lib/workflow-integrations/types"; + +export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit; + +export type TGetClientIdDTO = Omit; + +export type TCreateMicrosoftTeamsIntegrationDTO = Omit & { + tenantId: string; + slug: string; + redirectUri: string; + description?: string; + code: string; +}; + +export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit; + +export type TGetMicrosoftTeamsIntegrationByIdDTO = { id: string } & Omit; + +export type TUpdateMicrosoftTeamsIntegrationDTO = { id: string; slug?: string; description?: string } & Omit< + TOrgPermission, + "orgId" +>; + +export type TGetTeamsDTO = Omit & { + workflowIntegrationId: string; +}; + +export type TDeleteMicrosoftTeamsIntegrationDTO = { + id: string; +} & Omit; + +export type TSendNotificationDTO = { + tenantId: string; + microsoftTeamsIntegrationId: string; + orgId: string; + target: { + teamId: string; + channelIds: string[]; + }; + notification: TNotification; +}; diff --git a/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts new file mode 100644 index 000000000..918b96e89 --- /dev/null +++ b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts @@ -0,0 +1,28 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType; + +export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => { + const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs); + + const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => { + return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs) + .join( + TableName.MicrosoftTeamsIntegrations, + `${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`, + `${TableName.MicrosoftTeamsIntegrations}.id` + ) + .where("projectId", "=", projectId) + .select( + selectAllTableCols(TableName.ProjectMicrosoftTeamsConfigs), + selectAllTableCols(TableName.MicrosoftTeamsIntegrations) + ) + .first(); + }; + + return { ...projectMicrosoftTeamsConfigOrm, getIntegrationDetailsByProject }; +}; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 9f2de8a85..73a439055 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -43,6 +43,9 @@ import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns"; +import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; +import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; @@ -60,9 +63,11 @@ import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal"; +import { validateSlackChannelsField } from "../slack/slack-auth-validators"; import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; +import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; import { TProjectDALFactory } from "./project-dal"; import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; import { TProjectQueueFactory } from "./project-queue"; @@ -70,10 +75,11 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { TCreateProjectDTO, TDeleteProjectDTO, + TDeleteProjectWorkflowIntegration, TGetProjectDTO, TGetProjectKmsKey, - TGetProjectSlackConfig, TGetProjectSshConfig, + TGetProjectWorkflowIntegrationConfig, TListProjectAlertsDTO, TListProjectCasDTO, TListProjectCertificateTemplatesDTO, @@ -92,9 +98,9 @@ import { TUpdateProjectDTO, TUpdateProjectKmsDTO, TUpdateProjectNameDTO, - TUpdateProjectSlackConfig, TUpdateProjectSshConfig, TUpdateProjectVersionLimitDTO, + TUpdateProjectWorkflowIntegration, TUpgradeProjectDTO } from "./project-types"; @@ -123,8 +129,19 @@ type TProjectServiceFactoryDep = { "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" >; groupProjectDAL: Pick; - projectSlackConfigDAL: Pick; + projectSlackConfigDAL: Pick< + TProjectSlackConfigDALFactory, + "findOne" | "transaction" | "updateById" | "create" | "delete" + >; + projectMicrosoftTeamsConfigDAL: Pick< + TProjectMicrosoftTeamsConfigDALFactory, + "findOne" | "transaction" | "updateById" | "create" | "delete" + >; slackIntegrationDAL: Pick; + microsoftTeamsIntegrationDAL: Pick< + TMicrosoftTeamsIntegrationDALFactory, + "findById" | "findByIdWithWorkflowIntegrationDetails" + >; projectUserMembershipRoleDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; @@ -197,7 +214,9 @@ export const projectServiceFactory = ({ kmsService, projectBotDAL, projectSlackConfigDAL, + projectMicrosoftTeamsConfigDAL, slackIntegrationDAL, + microsoftTeamsIntegrationDAL, projectTemplateService, groupProjectDAL, smtpService @@ -1452,13 +1471,14 @@ export const projectServiceFactory = ({ return projectSshConfig; }; - const getProjectSlackConfig = async ({ + const getProjectWorkflowIntegrationConfig = async ({ actorId, actor, actorOrgId, actorAuthMethod, - projectId - }: TGetProjectSlackConfig) => { + projectId, + integration + }: TGetProjectWorkflowIntegrationConfig) => { const project = await projectDAL.findById(projectId); if (!project) { throw new NotFoundError({ @@ -1477,23 +1497,60 @@ export const projectServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); - return projectSlackConfigDAL.findOne({ - projectId: project.id + if (integration === WorkflowIntegration.SLACK) { + const config = await projectSlackConfigDAL.findOne({ + projectId: project.id + }); + + if (!config) { + throw new NotFoundError({ + message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found` + }); + } + + return { + ...config, + integration, + integrationId: config.slackIntegrationId + }; + } + + if (integration === WorkflowIntegration.MICROSOFT_TEAMS) { + const config = await projectMicrosoftTeamsConfigDAL.findOne({ + projectId: project.id + }); + + if (!config) { + throw new NotFoundError({ + message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found` + }); + } + + return { + ...config, + integration, + integrationId: config.microsoftTeamsIntegrationId + }; + } + + throw new BadRequestError({ + message: `Integration type '${integration as string}' not supported` }); }; - const updateProjectSlackConfig = async ({ + const updateProjectWorkflowIntegration = async ({ actorId, actor, actorOrgId, actorAuthMethod, projectId, - slackIntegrationId, + integration, + integrationId, isAccessRequestNotificationEnabled, accessRequestChannels, isSecretRequestNotificationEnabled, secretRequestChannels - }: TUpdateProjectSlackConfig) => { + }: TUpdateProjectWorkflowIntegration) => { const project = await projectDAL.findById(projectId); if (!project) { throw new NotFoundError({ @@ -1501,17 +1558,206 @@ export const projectServiceFactory = ({ }); } - const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId); - - if (!slackIntegration) { - throw new NotFoundError({ - message: `Slack integration with ID '${slackIntegrationId}' not found` + if (integration === WorkflowIntegration.SLACK) { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + + const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels); + const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels); + + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId); + + if (!slackIntegration) { + throw new NotFoundError({ + message: `Slack integration with ID '${integrationId}' not found` + }); + } + + if (slackIntegration.orgId !== actorOrgId) { + throw new ForbiddenRequestError({ + message: "Selected slack integration is not in the same organization" + }); + } + + if (slackIntegration.orgId !== project.orgId) { + throw new ForbiddenRequestError({ + message: "Selected slack integration is not in the same organization" + }); + } + + const updatedWorkflowIntegration = await projectSlackConfigDAL.transaction(async (tx) => { + const slackConfig = await projectSlackConfigDAL.findOne( + { + projectId + }, + tx + ); + + if (slackConfig) { + return projectSlackConfigDAL.updateById( + slackConfig.id, + { + slackIntegrationId: integrationId, + isAccessRequestNotificationEnabled, + accessRequestChannels: sanitizedAccessRequestChannels, + isSecretRequestNotificationEnabled, + secretRequestChannels: sanitizedSecretRequestChannels + }, + tx + ); + } + + return projectSlackConfigDAL.create( + { + projectId, + slackIntegrationId: integrationId, + isAccessRequestNotificationEnabled, + accessRequestChannels: sanitizedAccessRequestChannels, + isSecretRequestNotificationEnabled, + secretRequestChannels: sanitizedSecretRequestChannels + }, + tx + ); + }); + + return { + ...updatedWorkflowIntegration, + accessRequestChannels: sanitizedAccessRequestChannels, + secretRequestChannels: sanitizedSecretRequestChannels, + integrationId: slackIntegration.id, + integration: WorkflowIntegration.SLACK + } as const; + } + if (integration === WorkflowIntegration.MICROSOFT_TEAMS) { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + + if (isAccessRequestNotificationEnabled && !accessRequestChannels) { + throw new BadRequestError({ + message: "Access request channels are required when access request notifications are enabled" + }); + } + + if (isSecretRequestNotificationEnabled && !secretRequestChannels) { + throw new BadRequestError({ + message: "Secret request channels are required when secret request notifications are enabled" + }); + } + + if (!secretRequestChannels && !accessRequestChannels) { + throw new BadRequestError({ + message: "At least one of access request channels or secret request channels is required" + }); + } + + const microsoftTeamsIntegration = + await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId); + + if (!microsoftTeamsIntegration) { + throw new NotFoundError({ + message: `Microsoft Teams integration with ID '${integrationId}' not found` + }); + } + + if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) { + throw new BadRequestError({ + message: "Microsoft Teams integration is not properly installed in your tenant." + }); + } + + if (microsoftTeamsIntegration.orgId !== actorOrgId) { + throw new ForbiddenRequestError({ + message: "Selected Microsoft Teams integration is not in the same organization" + }); + } + + if (microsoftTeamsIntegration.orgId !== project.orgId) { + throw new ForbiddenRequestError({ + message: "Selected Microsoft Teams integration is not in the same organization" + }); + } + + const sanitizedAccessRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(accessRequestChannels); + const sanitizedSecretRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(secretRequestChannels); + + const updatedWorkflowIntegration = await projectMicrosoftTeamsConfigDAL.transaction(async (tx) => { + const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.findOne( + { + projectId + }, + tx + ); + + if (microsoftTeamsConfig) { + return projectMicrosoftTeamsConfigDAL.updateById( + microsoftTeamsConfig.id, + { + microsoftTeamsIntegrationId: integrationId, + isAccessRequestNotificationEnabled, + accessRequestChannels: sanitizedAccessRequestChannels || {}, + isSecretRequestNotificationEnabled, + secretRequestChannels: sanitizedSecretRequestChannels || {} + }, + tx + ); + } + + return projectMicrosoftTeamsConfigDAL.create( + { + projectId, + microsoftTeamsIntegrationId: integrationId, + isAccessRequestNotificationEnabled, + accessRequestChannels: sanitizedAccessRequestChannels || {}, + isSecretRequestNotificationEnabled, + secretRequestChannels: sanitizedSecretRequestChannels || {} + }, + tx + ); + }); + + return { + ...updatedWorkflowIntegration, + accessRequestChannels: sanitizedAccessRequestChannels, + secretRequestChannels: sanitizedSecretRequestChannels, + integrationId: microsoftTeamsIntegration.id, + integration: WorkflowIntegration.MICROSOFT_TEAMS + } as const; } - if (slackIntegration.orgId !== actorOrgId) { - throw new ForbiddenRequestError({ - message: "Selected slack integration is not in the same organization" + throw new BadRequestError({ + message: `Integration type '${integration as string}' not supported` + }); + }; + + const deleteProjectWorkflowIntegration = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId, + integrationId, + integration + }: TDeleteProjectWorkflowIntegration) => { + const project = await projectDAL.findById(projectId); + if (!project) { + throw new NotFoundError({ + message: `Project with ID '${projectId}' not found` }); } @@ -1524,47 +1770,28 @@ export const projectServiceFactory = ({ actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); - if (slackIntegration.orgId !== project.orgId) { - throw new ForbiddenRequestError({ - message: "Selected slack integration is not in the same organization" + if (integration === WorkflowIntegration.SLACK) { + const [deletedIntegration] = await projectSlackConfigDAL.delete({ + projectId, + slackIntegrationId: integrationId }); + + return deletedIntegration; } - return projectSlackConfigDAL.transaction(async (tx) => { - const slackConfig = await projectSlackConfigDAL.findOne( - { - projectId - }, - tx - ); + if (integration === WorkflowIntegration.MICROSOFT_TEAMS) { + const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({ + projectId, + microsoftTeamsIntegrationId: integrationId + }); - if (slackConfig) { - return projectSlackConfigDAL.updateById( - slackConfig.id, - { - slackIntegrationId, - isAccessRequestNotificationEnabled, - accessRequestChannels, - isSecretRequestNotificationEnabled, - secretRequestChannels - }, - tx - ); - } + return deletedIntegration; + } - return projectSlackConfigDAL.create( - { - projectId, - slackIntegrationId, - isAccessRequestNotificationEnabled, - accessRequestChannels, - isSecretRequestNotificationEnabled, - secretRequestChannels - }, - tx - ); + throw new BadRequestError({ + message: `Integration with ID '${integrationId}' not found` }); }; @@ -1673,10 +1900,11 @@ export const projectServiceFactory = ({ getProjectKmsBackup, loadProjectKmsBackup, getProjectKmsKeys, + getProjectWorkflowIntegrationConfig, + updateProjectWorkflowIntegration, + deleteProjectWorkflowIntegration, getProjectSshConfig, updateProjectSshConfig, - getProjectSlackConfig, - updateProjectSlackConfig, requestProjectAccess, searchProjects }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 274189668..dc26d2357 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -8,6 +8,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types"; enum KmsType { External = "external", @@ -166,14 +167,33 @@ export type TUpdateProjectSshConfig = { export type TGetProjectSshConfig = TProjectPermission; -export type TGetProjectSlackConfig = TProjectPermission; +export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & { + integration: WorkflowIntegration; +}; -export type TUpdateProjectSlackConfig = { - slackIntegrationId: string; - isAccessRequestNotificationEnabled: boolean; - accessRequestChannels: string; - isSecretRequestNotificationEnabled: boolean; - secretRequestChannels: string; +export type TUpdateProjectWorkflowIntegration = ( + | { + integrationId: string; + integration: WorkflowIntegration.SLACK; + isAccessRequestNotificationEnabled: boolean; + isSecretRequestNotificationEnabled: boolean; + accessRequestChannels?: string; + secretRequestChannels?: string; + } + | { + integrationId: string; + integration: WorkflowIntegration.MICROSOFT_TEAMS; + isAccessRequestNotificationEnabled: boolean; + isSecretRequestNotificationEnabled: boolean; + accessRequestChannels?: { teamId: string; channelIds: string[] }; + secretRequestChannels?: { teamId: string; channelIds: string[] }; + } +) & + TProjectPermission; + +export type TDeleteProjectWorkflowIntegration = { + integrationId: string; + integration: WorkflowIntegration; } & TProjectPermission; export type TBootstrapSshProjectDTO = { diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 6c84c0e76..d4f5e29cc 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -3,12 +3,10 @@ import { WebClient } from "@slack/web-api"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types"; -import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; -import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal"; -import { SlackTriggerFeature, TSlackNotification } from "./slack-types"; +import { TSendSlackNotificationDTO } from "./slack-types"; export const fetchSlackChannels = async (botKey: string) => { const slackChannels: { @@ -41,11 +39,11 @@ export const fetchSlackChannels = async (botKey: string) => { return slackChannels; }; -const buildSlackPayload = (notification: TSlackNotification) => { +const buildSlackPayload = (notification: TNotification) => { const appCfg = getConfig(); switch (notification.type) { - case SlackTriggerFeature.SECRET_APPROVAL: { + case TriggerFeature.SECRET_APPROVAL: { const { payload } = notification; const messageBody = `A secret approval request has been opened by ${payload.userEmail}. *Environment*: ${payload.environment} @@ -79,7 +77,7 @@ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId payloadBlocks }; } - case SlackTriggerFeature.ACCESS_REQUEST: { + case TriggerFeature.ACCESS_REQUEST: { const { payload } = notification; const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${ payload.isTemporary ? "temporary" : "permanent" @@ -125,51 +123,24 @@ User Note: ${payload.note}` } }; -export const triggerSlackNotification = async ({ - projectId, +export const sendSlackNotification = async ({ + orgId, notification, - projectSlackConfigDAL, - projectDAL, - kmsService -}: { - projectId: string; - notification: TSlackNotification; - projectSlackConfigDAL: Pick; - projectDAL: Pick; - kmsService: Pick; -}) => { - const { payloadMessage, payloadBlocks } = buildSlackPayload(notification); - const project = await projectDAL.findById(projectId); - const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id); - - if (!slackIntegration) { - return; - } - - let targetChannelIds: string[] = []; - if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) { - targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || []; - if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) { - return; - } - } else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) { - targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || []; - if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) { - return; - } - } - + kmsService, + targetChannelIds, + slackIntegration +}: TSendSlackNotificationDTO) => { const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: project.orgId + orgId }); - const botKey = orgDataKeyDecryptor({ cipherTextBlob: slackIntegration.encryptedBotAccessToken }).toString("utf8"); - const slackWebClient = new WebClient(botKey); + const { payloadMessage, payloadBlocks } = buildSlackPayload(notification); + for await (const conversationId of targetChannelIds) { // we send both text and blocks for compatibility with barebone clients await slackWebClient.chat diff --git a/backend/src/services/slack/slack-types.ts b/backend/src/services/slack/slack-types.ts index 3e8354adf..20395b1cd 100644 --- a/backend/src/services/slack/slack-types.ts +++ b/backend/src/services/slack/slack-types.ts @@ -1,4 +1,8 @@ +import { TSlackIntegrations } from "@app/db/schemas"; import { TOrgPermission } from "@app/lib/types"; +import { TNotification } from "@app/lib/workflow-integrations/types"; + +import { TKmsServiceFactory } from "../kms/kms-service"; export type TGetSlackInstallUrlDTO = { slug: string; @@ -48,34 +52,10 @@ export type TReinstallSlackIntegrationDTO = { slackBotUserId: string; }; -export enum SlackTriggerFeature { - SECRET_APPROVAL = "secret-approval", - ACCESS_REQUEST = "access-request" -} - -export type TSlackNotification = - | { - type: SlackTriggerFeature.SECRET_APPROVAL; - payload: { - userEmail: string; - environment: string; - secretPath: string; - requestId: string; - projectId: string; - secretKeys: string[]; - }; - } - | { - type: SlackTriggerFeature.ACCESS_REQUEST; - payload: { - requesterFullName: string; - requesterEmail: string; - isTemporary: boolean; - secretPath: string; - environment: string; - projectName: string; - permissions: string[]; - approvalUrl: string; - note?: string; - }; - }; +export type TSendSlackNotificationDTO = { + orgId: string; + notification: TNotification; + kmsService: Pick; + targetChannelIds: string[]; + slackIntegration: TSlackIntegrations; +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 317348cac..8c23a00c6 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -20,6 +20,7 @@ import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { RootKeyEncryptionStrategy } from "../kms/kms-types"; +import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service"; import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; @@ -47,6 +48,7 @@ type TSuperAdminServiceFactoryDep = { orgService: Pick; keyStore: Pick; licenseService: Pick; + microsoftTeamsService: Pick; }; export type TSuperAdminServiceFactory = ReturnType; @@ -77,7 +79,8 @@ export const superAdminServiceFactory = ({ licenseService, identityAccessTokenDAL, identityTokenAuthDAL, - identityOrgMembershipDAL + identityOrgMembershipDAL, + microsoftTeamsService }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -125,7 +128,13 @@ export const superAdminServiceFactory = ({ }; const updateServerCfg = async ( - data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string }, + data: TSuperAdminUpdate & { + slackClientId?: string; + slackClientSecret?: string; + microsoftTeamsAppId?: string; + microsoftTeamsClientSecret?: string; + microsoftTeamsBotId?: string; + }, userId: string ) => { const updatedData = data; @@ -192,10 +201,51 @@ export const superAdminServiceFactory = ({ updatedData.slackClientSecret = undefined; } + let microsoftTeamsSettingsUpdated = false; + if (data.microsoftTeamsAppId) { + const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId)); + + updatedData.encryptedMicrosoftTeamsAppId = encryptedClientId; + updatedData.microsoftTeamsAppId = undefined; + microsoftTeamsSettingsUpdated = true; + } + + if (data.microsoftTeamsClientSecret) { + const encryptedClientSecret = encryptWithRoot(Buffer.from(data.microsoftTeamsClientSecret)); + + updatedData.encryptedMicrosoftTeamsClientSecret = encryptedClientSecret; + updatedData.microsoftTeamsClientSecret = undefined; + microsoftTeamsSettingsUpdated = true; + } + + if (data.microsoftTeamsBotId) { + const encryptedBotId = encryptWithRoot(Buffer.from(data.microsoftTeamsBotId)); + + updatedData.encryptedMicrosoftTeamsBotId = encryptedBotId; + updatedData.microsoftTeamsBotId = undefined; + microsoftTeamsSettingsUpdated = true; + } const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); + if ( + updatedServerCfg.encryptedMicrosoftTeamsAppId && + updatedServerCfg.encryptedMicrosoftTeamsClientSecret && + updatedServerCfg.encryptedMicrosoftTeamsBotId && + microsoftTeamsSettingsUpdated + ) { + const decryptWithRoot = kmsService.decryptWithRootKey(); + decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsBotId); // validate that we're able to decrypt the bot ID + const decryptedAppId = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsAppId); + const decryptedAppPassword = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsClientSecret); + + await microsoftTeamsService.initializeTeamsBot({ + botAppId: decryptedAppId.toString(), + botAppPassword: decryptedAppPassword.toString() + }); + } + return updatedServerCfg; }; @@ -488,29 +538,40 @@ export const superAdminServiceFactory = ({ await userDAL.updateById(userId, { superAdmin: true }); }; - const getAdminSlackConfig = async () => { + const getAdminIntegrationsConfig = async () => { const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); if (!serverCfg) { throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" }); } - let clientId = ""; - let clientSecret = ""; - const decrypt = kmsService.decryptWithRootKey(); - if (serverCfg.encryptedSlackClientId) { - clientId = decrypt(serverCfg.encryptedSlackClientId).toString(); - } + const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : ""; + const slackClientSecret = serverCfg.encryptedSlackClientSecret + ? decrypt(serverCfg.encryptedSlackClientSecret).toString() + : ""; - if (serverCfg.encryptedSlackClientSecret) { - clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString(); - } + const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId + ? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString() + : ""; + const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret + ? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString() + : ""; + const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId + ? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString() + : ""; return { - clientId, - clientSecret + slack: { + clientSecret: slackClientSecret, + clientId: slackClientId + }, + microsoftTeams: { + appId: microsoftAppId, + clientSecret: microsoftClientSecret, + botId: microsoftBotId + } }; }; @@ -578,7 +639,7 @@ export const superAdminServiceFactory = ({ getUsers, deleteUser, getIdentities, - getAdminSlackConfig, + getAdminIntegrationsConfig, updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, grantServerAdminAccessToUser, diff --git a/backend/src/services/workflow-integration/workflow-integration-types.ts b/backend/src/services/workflow-integration/workflow-integration-types.ts index 9ae56b840..978b1e09e 100644 --- a/backend/src/services/workflow-integration/workflow-integration-types.ts +++ b/backend/src/services/workflow-integration/workflow-integration-types.ts @@ -1,7 +1,13 @@ import { TOrgPermission } from "@app/lib/types"; export enum WorkflowIntegration { - SLACK = "slack" + SLACK = "slack", + MICROSOFT_TEAMS = "microsoft-teams" +} + +export enum WorkflowIntegrationStatus { + PENDING = "pending", + INSTALLED = "installed" } export type TGetWorkflowIntegrationsByOrg = Omit; diff --git a/docs/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx b/docs/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx new file mode 100644 index 000000000..57f05984b --- /dev/null +++ b/docs/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx @@ -0,0 +1,192 @@ +--- +title: "Microsoft Teams Integration" +description: "Learn how to setup the Microsoft Teams integration" +--- + +import MicrosoftTeamsWorkflowIntegration from '/snippets/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx'; + + +This guide will provide step by step instructions on how to configure Microsoft Teams integration for your Infisical projects. + +## Setting up Microsoft Teams integration in your projects + + + + + + + ### Configure Azure Resources + To create a Microsoft Teams bot, you must first create an Azure Bot from the Azure Marketplace, an app registration, and a Microsoft Teams app. The steps below document in detail how to create and configure these resources. + + + + + Navigate to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/). + + Once you're on the Microsoft Teams Developer Portal, press the "Create a new app" button on the overview page. Give the bot a name and press the "Add" button. + + ![microsoft-dev-portal](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-app.png) + + + + After creating the Microsoft Teams app, you'll need to create a Microsoft Teams bot. + + Navigate to the app's bot settings page and click "Create a new bot". + + ![microsoft-dev-portal-create-bot](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-bot-app-feature.png) + ![microsoft-dev-portal-create-bot-2](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-a-new-bot.png) + + After clicking the "Create a new bot" button, you'll be navigated to the Teams Developer Portal for bot management. Press the "New bot" button, and enter the name of the bot. + Please keep in mind that the name of the bot can only contain alphanumeric characters, dashes, and underscores. + + ![microsoft-dev-portal-create-bot-3](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-bot.png) + + + + After creating the bot, you'll need to add a message endpoint to the bot. Navigate to the "Configure" tab, and input the following endpoint under "Endpoint address": + `https:///api/v1/workflow-integrations/microsoft-teams/message-endpoint` + Replace `` with the URL of your Infisical instance. + + Press the "Save" button to save the changes. + + ![microsoft-dev-portal-create-bot-4](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-add-bot-endpoint.png) + + + + When you create a bot through the Teams Developer Portal, an Azure App Registration is also created. + + Open your [Azure Portal](https://portal.azure.com/) and navigate to the "App Registrations" section to find the newly created app registration. + The name of the app registration will be the same as the name of the bot you created in the previous step. + + + Press the app registration to open the app registration overview page. + ![azure-app-registrations](/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registrations-bot.png) + + + + Navigate to the "API Permissions" section of the app registration, and add the following permissions: + - `AppCatalog.Read.All` + - `ChannelSettings.Read.All` + - `MultiTenantOrganization.Read.All` + - `Organization.Read.All` + - `Team.ReadBasic.All` + - `TeamsAppInstallation.Read.All` + + After adding the API permissions, press the "Grant admin consent" button to grant the permissions. + + ![azure-app-registration-api-permissions](/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-api-permissions.png) + + + + Navigate to the "Authentication" section of the App Registration, and press the "Add a platform" button. Select the "Web" platform and enter the following redirect URI: + `https:///organization/settings/oauth/callback`. Replace `` with the URL of your Infisical instance. + + ![azure-app-registration-register-callback](/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-register-callback.png) + + + + Next we need to get the application client ID and create a new client secret. **Save these values for later, as they're required to configure the Microsoft Teams integration in Infisical.** + + **Get the Application (Client) ID** + + To get the Application (Client) ID, press the "Copy" button next to the "Application (client) ID" field. + + ![copy-client-id](/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-copy-client-id.png) + + **Create a new client secret** + + Create a new client secret within the app registration. Navigate to the "Certificates & Secrets" section of the app registration, and press the "New client secret" button. + + ![create-client-secret](/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-create-client-secret.png) + + + Remember to rotate your client secret before it expires. Consider setting up a reminder or automated process to replace the secret and update your Infisical configuration before expiration. + + + + + Navigate back to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), and press the "Apps" tab and select the app you created earlier. + Here you can find the Microsoft Teams App ID in the overview page, which you need to copy and save for later. + + ![microsoft-teams-app-id](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-copy-app-id.png) + + + + + You need to link the Microsoft Teams App with the bot/app registration you created earlier. + Inside the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), navigate to the "Bot" tab and select the bot you created earlier. Navigate to the "App Features" section, and press the "Bot" button. + + Under the "What can your bot do?" section, enable `Only send notifications (one-way conversations)`. + + Under the "Select the scopes where people can use your bot" section, select `Personal`, `Team`, and `Group Chat`. + + Finally, press the "Save" button to save the changes. + + ![microsoft-teams-app-features](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-bot-app-feature.png) + ![microsoft-teams-configure-bot](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-configure-bot.png) + + + + To ensure that the Microsoft Teams App is working correctly, you can run an app validation test. This step is optional, but recommended to ensure the app is working correctly. + + You should expect to see two errors related to sending welcome messages, because we haven't configured the Microsoft Teams App inside Infisical yet, which is required for proactive messages. + + ![microsoft-teams-app-validation-test](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation.png) + + + You may see manifest validation errors. Before running an app validation test, you must ensure that your app has all errors resolved, such as having a description and a valid name. + + + ![microsoft-teams-app-validation-test-results](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation-result.png) + + + If you see two errors for bot welcome messages, you can ignore them. This is expected until you configure the Microsoft Teams App inside Infisical. + + + + + Once the Microsoft Teams App is working correctly, you can download the app package by navigating to the "Publish to Store" page, and pressing the "Download app package" button. + + ![microsoft-teams-download-app-package](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-download-app-package.png) + + + + ### Configure Microsoft Teams Bot in Infisical + + After creating the Microsoft Teams App and Bot, you are ready to configure the Microsoft Teams integration in Infisical. + Please note that you must be an instance admin in order to configure the Microsoft Teams instance-wide settings. + + + + ![server-admin-console-tab](/images/platform/workflow-integrations/microsoft-teams-integration/infisical-server-admin-console-tab.png) + ![infisical-instance-configure-microsoft-teams](/images/platform/workflow-integrations/microsoft-teams-integration/infisical-instance-configure-microsoft-teams.png) + + Enter the values you saved from the earlier steps into the respective fields. + + - **Application (Client) ID**: The Client ID of the App Registration from the previous steps. + - **Client Secret**: The Client Secret of the App Registration from the previous steps. + - **Microsoft Teams App ID**: The App ID of the Microsoft Teams App from the previous steps. + + Once completed, press the "Save" button to save your changes. + + + + + + + +## Troubleshooting + + + If you recently added the Microsoft Teams app to your tenant, **it may take up to 24 hours for Microsoft Teams to propagate the changes.** + A common indication of propagation issues is that the workflow integration is shown as "Installed", and you're able to view the teams and channels when configuring the workflow integration on your project, but no notification is being sent. + + + + The workflow integration can get stuck on Pending if you created the workflow integration before the Infisical Microsoft Teams bot was installed in the tenant. + To resolve this, make sure you have installed the Infisical Microsoft Teams app in your tenant. + + You can manually recheck the installation status by pressing the "Check Installation Status" button in the workflow organization settings. + + ![microsoft-teams-check-installation-status](/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-check-installation-status.png) + \ No newline at end of file diff --git a/docs/documentation/platform/workflow-integrations/slack-integration.mdx b/docs/documentation/platform/workflow-integrations/slack-integration.mdx index 92b3feeca..2317baabe 100644 --- a/docs/documentation/platform/workflow-integrations/slack-integration.mdx +++ b/docs/documentation/platform/workflow-integrations/slack-integration.mdx @@ -1,6 +1,6 @@ --- -title: "Slack integration" -description: "Learn how to setup Slack integration" +title: "Slack Integration" +description: "Learn how to setup the Slack integration" --- This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects. diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-api-permissions.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-api-permissions.png new file mode 100644 index 000000000..f3d0cbf8a Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-api-permissions.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-copy-client-id.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-copy-client-id.png new file mode 100644 index 000000000..ea4b7527f Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-copy-client-id.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-create-client-secret.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-create-client-secret.png new file mode 100644 index 000000000..34523a2ef Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-create-client-secret.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-register-callback.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-register-callback.png new file mode 100644 index 000000000..0ec40c0ae Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registration-register-callback.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registrations-bot.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registrations-bot.png new file mode 100644 index 000000000..50fea25d7 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/azure-app-registrations-bot.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-instance-configure-microsoft-teams.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-instance-configure-microsoft-teams.png new file mode 100644 index 000000000..efe3c02d7 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-instance-configure-microsoft-teams.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-add-microsoft-teams-integration.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-add-microsoft-teams-integration.png new file mode 100644 index 000000000..67eb9eb0a Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-add-microsoft-teams-integration.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-microsoft-teams-integration-modal.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-microsoft-teams-integration-modal.png new file mode 100644 index 000000000..d17ceb363 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-microsoft-teams-integration-modal.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-overview.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-overview.png new file mode 100644 index 000000000..f9f2176c8 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-overview.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-microsoft-teams-integration-save.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-microsoft-teams-integration-save.png new file mode 100644 index 000000000..10f80aa43 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-microsoft-teams-integration-save.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-settings.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-settings.png new file mode 100644 index 000000000..29f36ccb5 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-settings.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-server-admin-console-tab.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-server-admin-console-tab.png new file mode 100644 index 000000000..efd1bf488 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/infisical-server-admin-console-tab.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-consent-page.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-consent-page.png new file mode 100644 index 000000000..c15745363 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-consent-page.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-add-app.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-add-app.png new file mode 100644 index 000000000..718ece554 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-add-app.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-check-installation-status.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-check-installation-status.png new file mode 100644 index 000000000..5c71f2205 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-check-installation-status.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-install-app.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-install-app.png new file mode 100644 index 000000000..13e036b46 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-install-app.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-submit-app.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-submit-app.png new file mode 100644 index 000000000..6f2dd404f Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-submit-app.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-workflow-integration-created.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-workflow-integration-created.png new file mode 100644 index 000000000..22aa8adfa Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-workflow-integration-created.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-add-bot-endpoint.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-add-bot-endpoint.png new file mode 100644 index 000000000..139fa90b0 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-add-bot-endpoint.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation-result.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation-result.png new file mode 100644 index 000000000..b8279846b Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation-result.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation.png new file mode 100644 index 000000000..913b8505c Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-bot-app-feature.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-bot-app-feature.png new file mode 100644 index 000000000..e70694eab Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-bot-app-feature.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-configure-bot.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-configure-bot.png new file mode 100644 index 000000000..526356b34 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-configure-bot.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-copy-app-id.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-copy-app-id.png new file mode 100644 index 000000000..7e6fea75b Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-copy-app-id.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-a-new-bot.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-a-new-bot.png new file mode 100644 index 000000000..6c015acf7 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-a-new-bot.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-app.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-app.png new file mode 100644 index 000000000..ba307e1b9 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-app.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-bot.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-bot.png new file mode 100644 index 000000000..12be86084 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-create-bot.png differ diff --git a/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-download-app-package.png b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-download-app-package.png new file mode 100644 index 000000000..8a72a9243 Binary files /dev/null and b/docs/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-download-app-package.png differ diff --git a/docs/mint.json b/docs/mint.json index 2f84458ba..06091b281 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -222,7 +222,8 @@ { "group": "Workflow Integrations", "pages": [ - "documentation/platform/workflow-integrations/slack-integration" + "documentation/platform/workflow-integrations/slack-integration", + "documentation/platform/workflow-integrations/microsoft-teams-integration" ] }, { diff --git a/docs/snippets/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx b/docs/snippets/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx new file mode 100644 index 000000000..a6fc1be50 --- /dev/null +++ b/docs/snippets/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx @@ -0,0 +1,100 @@ + ### Create Microsoft Teams workflow integration + + + + Currently, Infisical requires you to install a custom Microsoft Teams app into your Microsoft Teams tenant. + + You can download the Infisical Microsoft Teams app package here: + - [Infisical Microsoft Teams app package](https://infisical-microsoft-teams-app.s3.us-east-1.amazonaws.com/Infisical.zip) + + + **Important for self-hosted users:** + + If you're self-hosting Infisical, you can skip the download step. Instead you should use the app package file you downloaded from the Microsoft Teams Developer Portal when you followed the Self-hosted guide. + + + Once you've downloaded the app package, you can install the app in your Microsoft Teams tenant by navigating to the **Apps** > **Upload a custom app** page, and selecting the "Upload an app" button. + + ![microsoft-teams-install-app](/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-install-app.png) + ![microsoft-teams-submit-app](/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-submit-app.png) + + Once the app has been submitted, your Microsoft Teams tenant admin will need to approve the app in the [Microsoft Teams Admin Center](https://admin.teams.microsoft.com/policies/manage-apps). + + + After the app has been approved, it can take a few hours _(up to 24 hours in some cases)_ for Microsoft Teams to reflect the new app. During this period, the Infisical app will not be visible in Microsoft Teams, and won't be usable. + + + Once the app has been approved, you will be able to use the Infisical Microsoft Teams integration in your projects. + + + + Once the app has been approved and installed in your Microsoft Teams tenant, you can add the app to your Microsoft Teams teams. + + ![microsoft-teams-add-app](/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-add-app.png) + + Navigate to **Apps** > **Built for your org**, select the "Infisical" app, and press the "Add" button to select the teams and channels you wish to add the app to. + + + This can also be done later through the [Microsoft Teams Admin Center](https://admin.teams.microsoft.com/policies/manage-apps), or through the Microsoft Teams client itself by navigating to the individual team's app settings. + + + Once the app has been added to the team, you will be able to use the Infisical Microsoft Teams integration in the team. + + + + + After installing the Microsoft Teams app, you are now ready to configure the Microsoft Teams integration within Infisical. + + Navigate to the **Workflow Integrations** tab in your organization settings, and press the "Add" button. + + ![org-integrations-overview](/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-overview.png) + + + + In order to use the Infisical Microsoft Teams integration, you will need to grant admin consent to the app. Once the consent is granted, the Microsoft Teams workflow integration will be created in your Infisical organization. + + Press the "Add" button and select the "Microsoft Teams" platform option. + ![add-microsoft-teams-integration](/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-add-microsoft-teams-integration.png) + + Select the Microsoft Teams integration you wish to configure, and press the "Configure" button. + + Here you will be prompted to enter an alias, tenant ID, and an optional description for your workflow integration. + The tenant ID is the ID of the Microsoft 365 / Azure AD tenant that you installed the Infisical Microsoft Teams app in, in the previous steps. + + ![configure-microsoft-teams-integration](/images/platform/workflow-integrations/microsoft-teams-integration/infisical-org-microsoft-teams-integration-modal.png) + + Press the "Create Microsoft Teams Integration" button, and you'll be navigated to the Azure AD consent page. + + ![microsoft-consent-page](/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-consent-page.png) + + + Please note that you must be a privileged administrator user of your Microsoft 365 / Azure AD tenant in order to grant admin consent to the app. + + + Once you've granted admin consent, you'll be navigated back to the Infisical organization settings, where you can now select the Microsoft Teams integration you just created. + + ![microsoft-teams-workflow-integration-created](/images/platform/workflow-integrations/microsoft-teams-integration/microsoft-teams-workflow-integration-created.png) + + + + ### Configure project to use Microsoft Teams workflow integration + + + + + To add a new Microsoft Teams workflow integration, navigate to **Project Settings** > **Workflow Integrations** and press the "Add". + ![project-settings-workflow-integrations](/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-settings.png) + + Select the "Microsoft Teams" option from the list of available workflow integrations. + + + + Your project will send notifications to the connected Microsoft Teams team of the + selected Microsoft Teams integration when the configured events are triggered. + + + Press the "Save" button to save your Microsoft Teams workflow integration. + ![infisical-project-microsoft-teams-integration-save](/images/platform/workflow-integrations/microsoft-teams-integration/infisical-project-microsoft-teams-integration-save.png) + + + Once you've created the project Microsoft Teams workflow integration, you will now receive Access Requests and Secret Approval Requests notifications in Microsoft Teams according to your configuration. \ No newline at end of file diff --git a/frontend/src/components/v2/Accordion/Accordion.tsx b/frontend/src/components/v2/Accordion/Accordion.tsx index 203ca3b2c..ad10314b9 100644 --- a/frontend/src/components/v2/Accordion/Accordion.tsx +++ b/frontend/src/components/v2/Accordion/Accordion.tsx @@ -47,8 +47,10 @@ AccordionTrigger.displayName = "AccordionTrigger"; export const AccordionContent = forwardRef< HTMLDivElement, - AccordionPrimitive.AccordionContentProps ->(({ children, className, ...props }, forwardedRef) => ( + AccordionPrimitive.AccordionContentProps & { + childrenClassName?: string; + } +>(({ children, className, childrenClassName, ...props }, forwardedRef) => ( -
{children}
+
{children}
)); diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index cd3c6675f..f44004d78 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -17,6 +17,12 @@ export const ROUTE_PATHS = Object.freeze({ PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup") }, Organization: { + Settings: { + OauthCallbackPage: setRoute( + "/organization/settings/oauth/callback", + "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" + ) + }, SecretScanning: setRoute( "/organization/secret-scanning", "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning" @@ -31,7 +37,7 @@ export const ROUTE_PATHS = Object.freeze({ ), SettingsPage: setRoute( "/organization/settings", - "/_authenticate/_inject-org-details/_org-layout/organization/settings" + "/_authenticate/_inject-org-details/_org-layout/organization/settings/" ), GroupDetailsByIDPage: setRoute( "/organization/groups/$groupId", diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index d43fbc080..bc812e18e 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -4,13 +4,12 @@ export { useAdminRemoveIdentitySuperAdminAccess, useCreateAdminUser, useRemoveUserServerAdminAccess, - useUpdateAdminSlackConfig, useUpdateServerConfig, useUpdateServerEncryptionStrategy } from "./mutation"; export { useAdminGetUsers, - useGetAdminSlackConfig, + useGetAdminIntegrationsConfig, useGetServerConfig, useGetServerRootKmsEncryptionDetails } from "./queries"; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index b3e1e37b4..2c88d4fd8 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -6,11 +6,10 @@ import { organizationKeys } from "../organization/queries"; import { User } from "../users/types"; import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { - AdminSlackConfig, RootKeyEncryptionStrategy, TCreateAdminUserDTO, TServerConfig, - TUpdateAdminSlackConfigDTO + TUpdateServerConfigDTO } from "./types"; export const useCreateAdminUser = () => { @@ -34,11 +33,7 @@ export const useCreateAdminUser = () => { export const useUpdateServerConfig = () => { const queryClient = useQueryClient(); - return useMutation< - TServerConfig, - object, - Partial - >({ + return useMutation({ mutationFn: async (opt) => { const { data } = await apiRequest.patch<{ config: TServerConfig }>( "/api/v1/admin/config", @@ -48,6 +43,7 @@ export const useUpdateServerConfig = () => { }, onSuccess: (data) => { queryClient.setQueryData(adminQueryKeys.serverConfig(), data); + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminIntegrationsConfig() }); queryClient.invalidateQueries({ queryKey: adminQueryKeys.serverConfig() }); queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations }); } @@ -119,23 +115,6 @@ export const useAdminGrantServerAdminAccess = () => { }); }; -export const useUpdateAdminSlackConfig = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (dto) => { - const { data } = await apiRequest.put( - "/api/v1/admin/integrations/slack/config", - dto - ); - - return data; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminSlackConfig() }); - } - }); -}; - export const useUpdateServerEncryptionStrategy = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index b24841dbd..1d44a93d0 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -7,7 +7,7 @@ import { User } from "../types"; import { AdminGetIdentitiesFilters, AdminGetUsersFilters, - AdminSlackConfig, + AdminIntegrationsConfig, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; @@ -22,7 +22,7 @@ export const adminQueryKeys = { getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, getIdentities: (filters: AdminGetIdentitiesFilters) => [adminStandaloneKeys.getIdentities, { filters }] as const, - getAdminSlackConfig: () => ["admin-slack-config"] as const, + getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const, getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const }; @@ -95,13 +95,11 @@ export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => { }); }; -export const useGetAdminSlackConfig = () => { +export const useGetAdminIntegrationsConfig = () => { return useQuery({ - queryKey: adminQueryKeys.getAdminSlackConfig(), + queryKey: adminQueryKeys.getAdminIntegrationsConfig(), queryFn: async () => { - const { data } = await apiRequest.get( - "/api/v1/admin/integrations/slack/config" - ); + const { data } = await apiRequest.get("/api/v1/admin/integrations"); return data; } diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 11f2cf44f..4533b5963 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -26,6 +26,14 @@ export type TServerConfig = { pageFrameContent?: string; }; +export type TUpdateServerConfigDTO = { + slackClientId?: string; + slackClientSecret?: string; + microsoftTeamsAppId?: string; + microsoftTeamsClientSecret?: string; + microsoftTeamsBotId?: string; +} & Partial; + export type TCreateAdminUserDTO = { email: string; password: string; @@ -42,11 +50,6 @@ export type TCreateAdminUserDTO = { salt: string; }; -export type TUpdateAdminSlackConfigDTO = { - clientId: string; - clientSecret: string; -}; - export type AdminGetUsersFilters = { limit: number; searchTerm: string; @@ -58,9 +61,16 @@ export type AdminGetIdentitiesFilters = { searchTerm: string; }; -export type AdminSlackConfig = { - clientId: string; - clientSecret: string; +export type AdminIntegrationsConfig = { + slack: { + clientId: string; + clientSecret: string; + }; + microsoftTeams: { + appId: string; + clientSecret: string; + botId: string; + }; }; export type TGetServerRootKmsEncryptionDetails = { diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 229f822da..9de5c4085 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -94,8 +94,10 @@ export const eventToNameMap: { [K in EventType]: string } = { "Create certificate template EST configuration", [EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]: "Update certificate template EST configuration", - [EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration", - [EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration", + [EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG]: + "Update project workflow integration configuration", + [EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG]: + "Get project workflow integration configuration", [EventType.INTEGRATION_SYNCED]: "Integration sync", [EventType.CREATE_SHARED_SECRET]: "Create shared secret", [EventType.DELETE_SHARED_SECRET]: "Delete shared secret", @@ -163,7 +165,22 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_SECRET_ROTATION]: "Create Secret Rotation", [EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation", [EventType.DELETE_SECRET_ROTATION]: "Delete Secret Rotation", - [EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated" + [EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated", + + [EventType.GET_PROJECT_SLACK_CONFIG]: "Get Project Slack Config", + [EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update Project Slack Config", + + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE]: + "Create Microsoft Teams Workflow Integration", + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE]: + "Delete Microsoft Teams Workflow Integration", + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE]: + "Update Microsoft Teams Workflow Integration", + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS]: + "Microsoft Teams Workflow Integration Check Installation Status", + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS]: "Get Microsoft Teams tenant teams", + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET]: "Get Microsoft Teams Workflow Integration", + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: "List Microsoft Teams Workflow Integration" }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index d465fb820..08f2559f6 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -98,8 +98,8 @@ export enum EventType { CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", - UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", - GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", + UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config", + GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config", INTEGRATION_SYNCED = "integration-synced", CREATE_SHARED_SECRET = "create-shared-secret", DELETE_SHARED_SECRET = "delete-shared-secret", @@ -163,5 +163,16 @@ export enum EventType { CREATE_SECRET_ROTATION = "create-secret-rotation", UPDATE_SECRET_ROTATION = "update-secret-rotation", DELETE_SECRET_ROTATION = "delete-secret-rotation", - SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets" + SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets", + + GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", + UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", + + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get", + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list" } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 2524f84f4..440f25c3d 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -1,6 +1,7 @@ import { CaStatus } from "../ca"; import { IdentityTrustedIp } from "../identities/types"; import { PkiItemType } from "../pkiCollections/constants"; +import { WorkflowIntegration } from "../workflowIntegrations/types"; import { ActorType, EventType, UserAgentType } from "./enums"; export type TGetAuditLogsFilter = { @@ -786,11 +787,12 @@ interface GetCertificateTemplateEstConfig { }; } -interface UpdateProjectSlackConfig { - type: EventType.UPDATE_PROJECT_SLACK_CONFIG; +interface UpdateProjectWorkflowIntegrationConfig { + type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG; metadata: { id: string; - slackIntegrationId: string; + integrationId: string; + integration: WorkflowIntegration; isAccessRequestNotificationEnabled: boolean; accessRequestChannels: string; isSecretRequestNotificationEnabled: boolean; @@ -798,10 +800,11 @@ interface UpdateProjectSlackConfig { }; } -interface GetProjectSlackConfig { - type: EventType.GET_PROJECT_SLACK_CONFIG; +interface GetProjectWorkflowIntegrationConfig { + type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG; metadata: { id: string; + integration: WorkflowIntegration; }; } @@ -898,8 +901,8 @@ export type Event = | UpdateCertificateTemplateEstConfig | CreateCertificateTemplateEstConfig | GetCertificateTemplateEstConfig - | UpdateProjectSlackConfig - | GetProjectSlackConfig + | UpdateProjectWorkflowIntegrationConfig + | GetProjectWorkflowIntegrationConfig | IntegrationSyncedEvent; export type AuditLog = { diff --git a/frontend/src/hooks/api/workflowIntegrations/index.ts b/frontend/src/hooks/api/workflowIntegrations/index.ts index e4730bd7d..f4e57d7cc 100644 --- a/frontend/src/hooks/api/workflowIntegrations/index.ts +++ b/frontend/src/hooks/api/workflowIntegrations/index.ts @@ -1,13 +1,2 @@ -export { - useDeleteSlackIntegration, - useUpdateProjectSlackConfig, - useUpdateSlackIntegration -} from "./mutation"; -export { - fetchSlackInstallUrl, - fetchSlackReinstallUrl, - useGetSlackIntegrationById, - useGetSlackIntegrationChannels, - useGetSlackIntegrations, - useGetWorkflowIntegrations -} from "./queries"; +export * from "./mutation"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx index e87b0dd6b..89c65f888 100644 --- a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx +++ b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx @@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/query-keys"; import { workflowIntegrationKeys } from "./queries"; import { + TCheckMicrosoftTeamsIntegrationInstallationStatusDTO, + TCreateMicrosoftTeamsIntegrationDTO, + TDeleteMicrosoftTeamsIntegrationDTO, + TDeleteProjectWorkflowIntegrationDTO, TDeleteSlackIntegrationDTO, - TUpdateProjectSlackConfigDTO, + TUpdateMicrosoftTeamsIntegrationDTO, + TUpdateProjectWorkflowIntegrationConfigDTO, TUpdateSlackIntegrationDTO } from "./types"; @@ -28,6 +33,47 @@ export const useUpdateSlackIntegration = () => { }); }; +export const useUpdateMicrosoftTeamsIntegration = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto) => { + const { data } = await apiRequest.patch( + `/api/v1/workflow-integrations/microsoft-teams/${dto.id}`, + dto + ); + + return data; + }, + onSuccess: (_, { orgId, id }) => { + queryClient.invalidateQueries({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id) + }); + queryClient.invalidateQueries({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId) + }); + queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) }); + } + }); +}; +export const useCreateMicrosoftTeamsIntegration = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/workflow-integrations/microsoft-teams", dto); + + return data; + }, + onSuccess: (_, { orgId }) => { + queryClient.invalidateQueries({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId) + }); + queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) }); + } + }); +}; + export const useDeleteSlackIntegration = () => { const queryClient = useQueryClient(); @@ -44,21 +90,86 @@ export const useDeleteSlackIntegration = () => { }); }; -export const useUpdateProjectSlackConfig = () => { +export const useDeleteMicrosoftTeamsIntegration = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete( + `/api/v1/workflow-integrations/microsoft-teams/${dto.id}` + ); + + return data; + }, + onSuccess: (_, { orgId, id }) => { + queryClient.invalidateQueries({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id) + }); + queryClient.invalidateQueries({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId) + }); + queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) }); + } + }); +}; + +export const useUpdateProjectWorkflowIntegrationConfig = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (dto: TUpdateProjectSlackConfigDTO) => { + mutationFn: async (dto: TUpdateProjectWorkflowIntegrationConfigDTO) => { const { data } = await apiRequest.put( - `/api/v1/workspace/${dto.workspaceId}/slack-config`, + `/api/v1/workspace/${dto.workspaceId}/workflow-integration`, dto ); return data; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { workspaceId, integration }) => { queryClient.invalidateQueries({ - queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId) + queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration) }); } }); }; + +export const useDeleteProjectWorkflowIntegration = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: TDeleteProjectWorkflowIntegrationDTO) => { + const { data } = await apiRequest.delete( + `/api/v1/workspace/${dto.projectId}/workflow-integration/${dto.integration}/${dto.integrationId}` + ); + + return data; + }, + onSuccess: (_, { projectId, integration }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(projectId, integration) + }); + } + }); +}; + +export const useCheckMicrosoftTeamsIntegrationInstallationStatus = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: TCheckMicrosoftTeamsIntegrationInstallationStatusDTO) => { + const { data } = await apiRequest.post( + `/api/v1/workflow-integrations/microsoft-teams/${dto.workflowIntegrationId}/installation-status` + ); + + return data; + }, + onSuccess: (_, { workflowIntegrationId, orgId }) => { + queryClient.invalidateQueries({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(workflowIntegrationId) + }); + queryClient.invalidateQueries({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId) + }); + queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) }); + } + }); +}; diff --git a/frontend/src/hooks/api/workflowIntegrations/queries.tsx b/frontend/src/hooks/api/workflowIntegrations/queries.tsx index bcf092c73..999215f1f 100644 --- a/frontend/src/hooks/api/workflowIntegrations/queries.tsx +++ b/frontend/src/hooks/api/workflowIntegrations/queries.tsx @@ -2,13 +2,29 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { SlackIntegration, SlackIntegrationChannel, WorkflowIntegration } from "./types"; +import { + MicrosoftTeamsIntegration, + MicrosoftTeamsIntegrationTeam, + SlackIntegration, + SlackIntegrationChannel, + WorkflowIntegration +} from "./types"; export const workflowIntegrationKeys = { getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"], getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"], getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"], - getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"] + getMicrosoftTeamsIntegrations: (orgId?: string) => [ + { orgId }, + "microsoft-teams-workflow-integrations" + ], + getMicrosoftTeamsIntegration: (id?: string) => [{ id }, "microsoft-teams-workflow-integration"], + getMicrosoftTeamsIntegrationTeams: (id?: string) => [ + { id }, + "microsoft-teams-workflow-integration-teams" + ], + getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"], + getMicrosoftTeamsClientId: () => ["microsoft-teams-client-id"] }; export const fetchSlackInstallUrl = async ({ @@ -66,13 +82,58 @@ export const fetchWorkflowIntegrations = async () => { return data; }; -export const useGetSlackIntegrations = (orgId?: string) => +export const fetchMicrosoftTeamsIntegrations = async () => { + const { data } = await apiRequest.get( + "/api/v1/workflow-integrations/microsoft-teams" + ); + + return data; +}; + +export const fetchMicrosoftTeamsIntegrationById = async (id?: string) => { + const { data } = await apiRequest.get( + `/api/v1/workflow-integrations/microsoft-teams/${id}` + ); + + return data; +}; +export const fetchMicrosoftTeamsIntegrationTeams = async (id?: string) => { + const { data } = await apiRequest.get( + `/api/v1/workflow-integrations/microsoft-teams/${id}/teams` + ); + + return data; +}; + +export const fetchMicrosoftTeamsClientId = async () => { + const { data } = await apiRequest.get<{ clientId: string }>( + "/api/v1/workflow-integrations/microsoft-teams/client-id" + ); + + return data; +}; + +export const useGetMicrosoftTeamsIntegrations = (orgId?: string) => useQuery({ - queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId), - queryFn: () => fetchSlackIntegrations(), + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId), + queryFn: () => fetchMicrosoftTeamsIntegrations(), enabled: Boolean(orgId) }); +export const useGetMicrosoftTeamsIntegrationById = (id?: string) => + useQuery({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id), + queryFn: () => fetchMicrosoftTeamsIntegrationById(id), + enabled: Boolean(id) + }); + +export const useGetMicrosoftTeamsIntegrationTeams = (id?: string) => + useQuery({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrationTeams(id), + queryFn: () => fetchMicrosoftTeamsIntegrationTeams(id), + enabled: Boolean(id) + }); + export const useGetSlackIntegrationById = (id?: string) => useQuery({ queryKey: workflowIntegrationKeys.getSlackIntegration(id), @@ -93,3 +154,10 @@ export const useGetWorkflowIntegrations = (id?: string) => queryFn: () => fetchWorkflowIntegrations(), enabled: Boolean(id) }); + +export const useGetMicrosoftTeamsClientId = () => + useQuery({ + queryKey: workflowIntegrationKeys.getMicrosoftTeamsClientId(), + queryFn: () => fetchMicrosoftTeamsClientId(), + enabled: true + }); diff --git a/frontend/src/hooks/api/workflowIntegrations/types.ts b/frontend/src/hooks/api/workflowIntegrations/types.ts index e30193776..4d2baf4bf 100644 --- a/frontend/src/hooks/api/workflowIntegrations/types.ts +++ b/frontend/src/hooks/api/workflowIntegrations/types.ts @@ -1,14 +1,25 @@ export enum WorkflowIntegrationPlatform { - SLACK = "slack" + SLACK = "slack", + MICROSOFT_TEAMS = "microsoft-teams" } export type WorkflowIntegration = { id: string; slug: string; description: string; + status: WorkflowIntegrationStatus; integration: WorkflowIntegrationPlatform; }; +export type MicrosoftTeamsIntegrationTeam = { + teamId: string; + teamName: string; + channels: { + channelId: string; + channelName: string; + }[]; +}; + export type SlackIntegration = { id: string; slug: string; @@ -16,6 +27,18 @@ export type SlackIntegration = { teamName: string; }; +export enum WorkflowIntegrationStatus { + Pending = "pending", + Installed = "installed" +} + +export type MicrosoftTeamsIntegration = { + id: string; + slug: string; + description: string; + tenantId: string; +}; + export type SlackIntegrationChannel = { id: string; name: string; @@ -28,25 +51,90 @@ export type TUpdateSlackIntegrationDTO = { description?: string; }; +export type TUpdateMicrosoftTeamsIntegrationDTO = { + id: string; + orgId: string; + slug?: string; + description?: string; +}; + +export type TCreateMicrosoftTeamsIntegrationDTO = { + code: string; + tenantId: string; + slug: string; + description?: string; + redirectUri: string; + orgId: string; +}; + export type TDeleteSlackIntegrationDTO = { id: string; orgId: string; }; -export type ProjectSlackConfig = { +export type TDeleteMicrosoftTeamsIntegrationDTO = { id: string; - slackIntegrationId: string; - isAccessRequestNotificationEnabled: boolean; - accessRequestChannels: string; - isSecretRequestNotificationEnabled: boolean; - secretRequestChannels: string; + orgId: string; }; -export type TUpdateProjectSlackConfigDTO = { - workspaceId: string; - slackIntegrationId: string; - isAccessRequestNotificationEnabled: boolean; - accessRequestChannels: string; - isSecretRequestNotificationEnabled: boolean; - secretRequestChannels: string; +export type ProjectWorkflowIntegrationConfig = + | { + id: string; + integration: WorkflowIntegrationPlatform.SLACK; + integrationId: string; + isAccessRequestNotificationEnabled: boolean; + accessRequestChannels: string; + isSecretRequestNotificationEnabled: boolean; + secretRequestChannels: string; + } + | { + id: string; + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS; + integrationId: string; + isAccessRequestNotificationEnabled: boolean; + isSecretRequestNotificationEnabled: boolean; + accessRequestChannels: { + teamId: string; + channelIds: string[]; + }; + secretRequestChannels: { + teamId: string; + channelIds: string[]; + }; + }; + +export type TUpdateProjectWorkflowIntegrationConfigDTO = + | { + integration: WorkflowIntegrationPlatform.SLACK; + workspaceId: string; + integrationId: string; + isAccessRequestNotificationEnabled: boolean; + accessRequestChannels: string; + isSecretRequestNotificationEnabled: boolean; + secretRequestChannels: string; + } + | { + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS; + workspaceId: string; + integrationId: string; + isAccessRequestNotificationEnabled: boolean; + isSecretRequestNotificationEnabled: boolean; + accessRequestChannels?: { + teamId: string; + channelIds: string[]; + }; + secretRequestChannels?: { + teamId: string; + channelIds: string[]; + }; + }; +export type TDeleteProjectWorkflowIntegrationDTO = { + projectId: string; + integration: WorkflowIntegrationPlatform; + integrationId: string; +}; + +export type TCheckMicrosoftTeamsIntegrationInstallationStatusDTO = { + workflowIntegrationId: string; + orgId: string; }; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index c4defb68d..a7b402d3c 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -26,9 +26,9 @@ export { useGetWorkspaceIndexStatus, useGetWorkspaceIntegrations, useGetWorkspaceSecrets, - useGetWorkspaceSlackConfig, useGetWorkspaceUserDetails, useGetWorkspaceUsers, + useGetWorkspaceWorkflowIntegrationConfig, useListWorkspaceCas, useListWorkspaceCertificates, useListWorkspaceCertificateTemplates, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 0a2bf491b..c869bd66f 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -20,7 +20,10 @@ import { TSshCertificateTemplate } from "../sshCertificateTemplates/types"; import { TSshHost } from "../sshHost/types"; import { userKeys } from "../users/query-keys"; import { TWorkspaceUser } from "../users/types"; -import { ProjectSlackConfig } from "../workflowIntegrations/types"; +import { + ProjectWorkflowIntegrationConfig, + WorkflowIntegrationPlatform +} from "../workflowIntegrations/types"; import { workspaceKeys } from "./query-keys"; import { CreateEnvironmentDTO, @@ -883,13 +886,27 @@ export const useListWorkspaceSshCertificateTemplates = (projectId: string) => { }); }; -export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => { +export const useGetWorkspaceWorkflowIntegrationConfig = ({ + workspaceId, + integration +}: { + workspaceId: string; + integration: WorkflowIntegrationPlatform; +}) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId), + queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration), queryFn: async () => { - const { data } = await apiRequest.get( - `/api/v1/workspace/${workspaceId}/slack-config` - ); + const { data } = await apiRequest + .get( + `/api/v1/workspace/${workspaceId}/workflow-integration-config/${integration}` + ) + .catch((err) => { + if (err.response.status === 404) { + return { data: null }; + } + + throw err; + }); return data; }, diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index 05e9c7588..fb2d30ce8 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -1,6 +1,7 @@ import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types"; import type { CaStatus } from "../ca"; +import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types"; export const workspaceKeys = { getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const, @@ -54,8 +55,10 @@ export const workspaceKeys = { [{ workspaceId }, "workspace-pki-collections"] as const, getWorkspaceCertificateTemplates: (workspaceId: string) => [{ workspaceId }, "workspace-certificate-templates"] as const, - getWorkspaceSlackConfig: (workspaceId: string) => - [{ workspaceId }, "workspace-slack-config"] as const, + getWorkspaceWorkflowIntegrationConfig: ( + workspaceId: string, + integration: WorkflowIntegrationPlatform + ) => [{ workspaceId, integration }, "workspace-workflow-integration-config"] as const, getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const, allWorkspaceSshCertificates: (projectId: string) => [{ projectId }, "workspace-ssh-certificates"] as const, diff --git a/frontend/src/index.css b/frontend/src/index.css index 1c5b0c465..3b00a91da 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -197,4 +197,4 @@ html { position: absolute; top: 0.5rem; @apply text-sm text-gray-500 opacity-50; -} +} \ No newline at end of file diff --git a/frontend/src/pages/admin/OverviewPage/components/IntegrationPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IntegrationPanel.tsx index 9bb005ce4..2ef4f7329 100644 --- a/frontend/src/pages/admin/OverviewPage/components/IntegrationPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/IntegrationPanel.tsx @@ -1,166 +1,24 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; +import { useGetAdminIntegrationsConfig } from "@app/hooks/api"; -import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; -import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api"; - -const slackFormSchema = z.object({ - clientId: z.string(), - clientSecret: z.string() -}); - -type TSlackForm = z.infer; - -const getCustomSlackAppCreationUrl = () => - `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent( - JSON.stringify({ - display_information: { - name: "Infisical", - description: "Get real-time Infisical updates in Slack", - background_color: "#c2d62b", - long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack. - - Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice. - - Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent. - - Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects. - - By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.` - }, - features: { - app_home: { - home_tab_enabled: false, - messages_tab_enabled: false, - messages_tab_read_only_enabled: true - }, - bot_user: { - display_name: "Infisical", - always_online: true - } - }, - oauth_config: { - redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect`], - scopes: { - bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"] - } - }, - settings: { - org_deploy_enabled: false, - socket_mode_enabled: false, - token_rotation_enabled: false - } - }) - )}`; +import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm"; +import { SlackIntegrationForm } from "./SlackIntegrationForm"; export const IntegrationPanel = () => { - const { - control, - handleSubmit, - setValue, - formState: { isSubmitting, isDirty } - } = useForm({ - resolver: zodResolver(slackFormSchema) - }); - - const { data: adminSlackConfig } = useGetAdminSlackConfig(); - const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); - const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle(); - const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle(); - - useEffect(() => { - if (adminSlackConfig) { - setValue("clientId", adminSlackConfig.clientId); - setValue("clientSecret", adminSlackConfig.clientSecret); - } - }, [adminSlackConfig]); - - const onSlackFormSubmit = async (data: TSlackForm) => { - await updateAdminServerConfig({ - slackClientId: data.clientId, - slackClientSecret: data.clientSecret - }); - - createNotification({ - text: "Updated admin slack configuration", - type: "success" - }); - }; + const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig(); return ( -
-
-
Slack Integration
-
- Step 1: Create your Infisical Slack App +
+
+
Integrations
+
+ Configure your instance-wide settings to enable integration with Slack and Microsoft + Teams.
-
- -
-
- Step 2: Configure your instance-wide settings to enable integration with Slack. Copy the - values from the App Credentials page of your custom Slack App. -
- ( - - setIsSlackClientIdFocused.on()} - onBlur={() => setIsSlackClientIdFocused.off()} - onChange={(e) => field.onChange(e.target.value)} - /> - - )} - /> - ( - - setIsSlackClientSecretFocused.on()} - onBlur={() => setIsSlackClientSecretFocused.off()} - onChange={(e) => field.onChange(e.target.value)} - /> - - )} - />
- - +
+ + +
+
); }; diff --git a/frontend/src/pages/admin/OverviewPage/components/MicrosoftTeamsIntegrationForm.tsx b/frontend/src/pages/admin/OverviewPage/components/MicrosoftTeamsIntegrationForm.tsx new file mode 100644 index 000000000..ecd8288f4 --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/MicrosoftTeamsIntegrationForm.tsx @@ -0,0 +1,183 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { BsMicrosoftTeams } from "react-icons/bs"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + FormControl, + Input +} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { useUpdateServerConfig } from "@app/hooks/api"; +import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types"; + +const microsoftTeamsFormSchema = z.object({ + appId: z.string(), + clientSecret: z.string(), + botId: z.string() +}); + +type TMicrosoftTeamsForm = z.infer; + +type Props = { + adminIntegrationsConfig?: AdminIntegrationsConfig; +}; + +export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props) => { + const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); + const [isMicrosoftTeamsAppIdFocused, setIsMicrosoftTeamsAppIdFocused] = useToggle(); + const [isMicrosoftTeamsClientSecretFocused, setIsMicrosoftTeamsClientSecretFocused] = useToggle(); + const [isMicrosoftTeamsBotIdFocused, setIsMicrosoftTeamsBotIdFocused] = useToggle(); + const { + control, + handleSubmit, + setValue, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(microsoftTeamsFormSchema) + }); + + const onSubmit = async (data: TMicrosoftTeamsForm) => { + await updateAdminServerConfig({ + microsoftTeamsAppId: data.appId, + microsoftTeamsClientSecret: data.clientSecret, + microsoftTeamsBotId: data.botId + }); + + createNotification({ + text: "Updated admin Microsoft Teams configuration", + type: "success" + }); + }; + + useEffect(() => { + if (adminIntegrationsConfig) { + setValue("appId", adminIntegrationsConfig.microsoftTeams.appId); + setValue("clientSecret", adminIntegrationsConfig.microsoftTeams.clientSecret); + setValue("botId", adminIntegrationsConfig.microsoftTeams.botId); + } + }, [adminIntegrationsConfig]); + + return ( +
+ + + +
+ +
Microsoft Teams Integration
+
+
+ +
+
+ Step 1: Create and configure Microsoft Teams bot and Azure Resources. Please refer + to the documentation below for more information. +
+ +
+ Step 2: Configure your instance-wide settings to enable integration with Microsoft + Teams. Copy the App ID and Client Secret from your Microsoft Teams bot's App + Registration page. The Client Secret is the password for the bot. +
+ ( + + setIsMicrosoftTeamsAppIdFocused.on()} + onBlur={() => setIsMicrosoftTeamsAppIdFocused.off()} + onChange={(e) => field.onChange(e.target.value)} + /> + + )} + /> + ( + + setIsMicrosoftTeamsClientSecretFocused.on()} + onBlur={() => setIsMicrosoftTeamsClientSecretFocused.off()} + onChange={(e) => field.onChange(e.target.value)} + /> + + )} + /> + + ( + + setIsMicrosoftTeamsBotIdFocused.on()} + onBlur={() => setIsMicrosoftTeamsBotIdFocused.off()} + onChange={(e) => field.onChange(e.target.value)} + /> + + )} + /> +
+ +
+
+
+
+
+
+ ); +}; diff --git a/frontend/src/pages/admin/OverviewPage/components/SlackIntegrationForm.tsx b/frontend/src/pages/admin/OverviewPage/components/SlackIntegrationForm.tsx new file mode 100644 index 000000000..63da0e384 --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/SlackIntegrationForm.tsx @@ -0,0 +1,189 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { BsSlack } from "react-icons/bs"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + FormControl, + Input +} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { useUpdateServerConfig } from "@app/hooks/api"; +import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types"; + +const getCustomSlackAppCreationUrl = () => + `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent( + JSON.stringify({ + display_information: { + name: "Infisical", + description: "Get real-time Infisical updates in Slack", + background_color: "#c2d62b", + long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack. + - Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice. + - Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent. + - Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects. + + By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.` + }, + features: { + app_home: { + home_tab_enabled: false, + messages_tab_enabled: false, + messages_tab_read_only_enabled: true + }, + bot_user: { + display_name: "Infisical", + always_online: true + } + }, + oauth_config: { + redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect`], + scopes: { + bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"] + } + }, + settings: { + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: false + } + }) + )}`; + +const slackFormSchema = z.object({ + clientId: z.string(), + clientSecret: z.string() +}); + +type TSlackForm = z.infer; + +type Props = { + adminIntegrationsConfig?: AdminIntegrationsConfig; +}; + +export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => { + const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); + const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle(); + const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle(); + + const { + control, + handleSubmit, + setValue, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(slackFormSchema) + }); + + const onSubmit = async (data: TSlackForm) => { + await updateAdminServerConfig({ + slackClientId: data.clientId, + slackClientSecret: data.clientSecret + }); + + createNotification({ + text: "Updated admin slack configuration", + type: "success" + }); + }; + + useEffect(() => { + if (adminIntegrationsConfig) { + setValue("clientId", adminIntegrationsConfig.slack.clientId); + setValue("clientSecret", adminIntegrationsConfig.slack.clientSecret); + } + }, [adminIntegrationsConfig]); + + return ( +
+ + + +
+ +
Slack Integration
+
+
+ +
+
+ Step 1: Create your Infisical Slack App +
+
+ +
+
+ Step 2: Configure your instance-wide settings to enable integration with Slack. Copy + the values from the App Credentials page of your custom Slack App. +
+ ( + + setIsSlackClientIdFocused.on()} + onBlur={() => setIsSlackClientIdFocused.off()} + onChange={(e) => field.onChange(e.target.value)} + /> + + )} + /> + ( + + setIsSlackClientSecretFocused.on()} + onBlur={() => setIsSlackClientSecretFocused.off()} + onChange={(e) => field.onChange(e.target.value)} + /> + + )} + /> +
+ +
+
+
+
+
+
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx new file mode 100644 index 000000000..df5bf4f60 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx @@ -0,0 +1,97 @@ +import { useCallback, useEffect, useState } from "react"; +import { useNavigate, useSearch } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { ContentLoader } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { useOrganization } from "@app/context"; +import { useCreateMicrosoftTeamsIntegration } from "@app/hooks/api"; + +const stateSchema = z.object({ + redirectUri: z.string(), + tenantId: z.string(), + slug: z.string(), + description: z.string().optional(), + csrfToken: z.string(), + clientId: z.string() +}); + +export const OAuthCallbackPage = () => { + const navigate = useNavigate(); + const { currentOrg } = useOrganization(); + const [isReady, setIsReady] = useState(false); + + const search = useSearch({ + from: ROUTE_PATHS.Organization.Settings.OauthCallbackPage.id + }); + + const createMicrosoftTeamsWorkflowIntegration = useCreateMicrosoftTeamsIntegration(); + + const { state: rawState, code } = search; + + const state = stateSchema.parse(rawState); + + const clearState = () => { + if (state.csrfToken !== localStorage.getItem("latestCSRFToken")) { + throw new Error("Invalid CSRF token"); + } + + localStorage.removeItem("latestCSRFToken"); + }; + + const handleMicrosoftTeams = useCallback(async () => { + clearState(); + + if (!code) { + throw new Error("No code provided"); + } + + await createMicrosoftTeamsWorkflowIntegration.mutateAsync({ + orgId: currentOrg.id, + tenantId: state.tenantId, + code, + slug: state.slug, + description: state.description ?? "", + redirectUri: state.redirectUri + }); + + navigate({ + to: ROUTE_PATHS.Organization.SettingsPage.path + }); + }, []); + + // Ensure that the localstorage is ready for use, to avoid the form data being malformed + useEffect(() => { + if (!isReady) { + setIsReady(!!localStorage.length); + } + }, [localStorage.length]); + + useEffect(() => { + if (!isReady) return; + + (async () => { + try { + await handleMicrosoftTeams(); + + createNotification({ + text: "Successfully created Microsoft Teams workflow integration", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to create Microsoft Teams workflow integration", + type: "error" + }); + } + })(); + }, [isReady]); + + return ( +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/route.tsx b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/route.tsx new file mode 100644 index 000000000..8c6af10f3 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/route.tsx @@ -0,0 +1,27 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { OAuthCallbackPage } from "./OauthCallbackPage"; + +const SettingsOAuthCallbackPageQueryParamsSchema = z.object({ + state: z + .object({ + clientId: z.string(), + tenantId: z.string(), + slug: z.string(), + description: z.string().optional(), + redirectUri: z.string(), + csrfToken: z.string() + }) + .nullable() + .catch(null), + code: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" +)({ + component: OAuthCallbackPage, + validateSearch: zodValidator(SettingsOAuthCallbackPageQueryParamsSchema) +}); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/AddWorkflowIntegrationForm.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/AddWorkflowIntegrationForm.tsx index 1d12b50dd..d4934ce7e 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/AddWorkflowIntegrationForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/AddWorkflowIntegrationForm.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { BsMicrosoftTeams } from "react-icons/bs"; import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; @@ -6,6 +7,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; +import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm"; import { SlackIntegrationForm } from "./SlackIntegrationForm"; type Props = { @@ -20,9 +22,14 @@ enum WizardSteps { const PLATFORM_LIST = [ { - icon: faSlack, + icon: , platform: WorkflowIntegrationPlatform.SLACK, title: "Slack" + }, + { + icon: , + platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS, + title: "Microsoft Teams" } ]; @@ -38,7 +45,10 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => { return ( handleFormReset(state)}> - + char.toUpperCase()) ?? "workflow"} integration`} + className="my-4" + > {wizardStep === WizardSteps.SelectPlatform && ( { } }} > - +
{icon}
{title}
))} @@ -86,6 +96,18 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => { onToggle(false)} /> )} + {wizardStep === WizardSteps.PlatformInputs && + selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && ( + + onToggle(false)} /> + + )} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx index a9af20247..48cfa2243 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx @@ -1,6 +1,7 @@ import { Modal, ModalContent } from "@app/components/v2"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; +import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm"; import { SlackIntegrationForm } from "./SlackIntegrationForm"; type Props = { @@ -20,6 +21,9 @@ export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatf {workflowPlatform === WorkflowIntegrationPlatform.SLACK && ( onOpenChange(false)} /> )} + {workflowPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && ( + onOpenChange(false)} /> + )} ); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/MicrosoftTeamsIntegrationForm.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/MicrosoftTeamsIntegrationForm.tsx new file mode 100644 index 000000000..09e5d6583 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/MicrosoftTeamsIntegrationForm.tsx @@ -0,0 +1,192 @@ +import crypto from "crypto"; + +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetMicrosoftTeamsClientId, + useGetMicrosoftTeamsIntegrationById, + useUpdateMicrosoftTeamsIntegration +} from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +type Props = { + id?: string; + onClose: () => void; +}; + +const microsoftTeamsFormSchema = z.object({ + slug: slugSchema({ min: 1, field: "Alias" }), + tenantId: z + .string() + .min(1, { message: "Tenant ID is required" }) + .trim() + .uuid("Tenant ID must be a valid UUID"), + description: z.string().optional() +}); + +type TMicrosoftTeamsFormData = z.infer; + +export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => { + const { + control, + handleSubmit, + setValue, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(microsoftTeamsFormSchema) + }); + + const { currentOrg } = useOrganization(); + const { data: microsoftTeamsIntegration } = useGetMicrosoftTeamsIntegrationById(id); + const { mutateAsync: updateMicrosoftTeamsIntegration } = useUpdateMicrosoftTeamsIntegration(); + const { data: microsoftTeamsClientId } = useGetMicrosoftTeamsClientId(); + + useEffect(() => { + if (microsoftTeamsIntegration) { + setValue("slug", microsoftTeamsIntegration.slug); + setValue("description", microsoftTeamsIntegration.description ?? ""); + setValue("tenantId", microsoftTeamsIntegration.tenantId); + } + }, [microsoftTeamsIntegration]); + + const handleMicrosoftTeamsFormSubmit = async ({ + slug, + description, + tenantId + }: TMicrosoftTeamsFormData) => { + if (!microsoftTeamsClientId) { + createNotification({ + text: "Microsoft Teams client ID is not set. Please contact your instance administrator.", + type: "error" + }); + return; + } + + if (id && microsoftTeamsIntegration) { + if (!currentOrg) { + return; + } + + if (tenantId !== microsoftTeamsIntegration.tenantId) { + createNotification({ + text: "Tenant ID cannot be changed", + type: "error" + }); + return; + } + + await updateMicrosoftTeamsIntegration({ + id, + orgId: currentOrg.id, + slug, + description + }); + + createNotification({ + text: "Successfully updated Microsoft Teams integration", + type: "success" + }); + + onClose(); + } else { + const csrfToken = crypto.randomBytes(32).toString("hex"); + localStorage.setItem("latestCSRFToken", csrfToken); + + const state = { + redirectUri: `${window.location.origin}/organization/settings/oauth/callback`, + tenantId, + slug, + description, + csrfToken, + clientId: microsoftTeamsClientId.clientId + }; + + const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize? + client_id=${microsoftTeamsClientId.clientId} + &redirect_uri=${state.redirectUri} + &response_type=code + &response_mode=query + &scope=https://graph.microsoft.com/.default + &state=${encodeURIComponent(JSON.stringify(state))} + &prompt=consent + &admin_consent=true`; + + window.location.href = url; + } + }; + + return ( +
+
+ For seamless installations, ensure that the Infisical bot is already installed in your + Microsoft Teams tenant. For more information, please refer to the{" "} + + Microsoft Teams Workflow Integration Documentation + + , which will guide you through the download and installation process. +
+ ( + + + + )} + /> + {!microsoftTeamsIntegration && ( + ( + + + + )} + /> + )} + ( + + + + )} + /> + {microsoftTeamsIntegration && ( + + + + )} +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx index e3b59489c..374c679a1 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx @@ -1,5 +1,5 @@ -import { faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { BsMicrosoftTeams, BsSlack } from "react-icons/bs"; +import { faEllipsis, faGear, faInfoCircle, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import axios from "axios"; import { twMerge } from "tailwind-merge"; @@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + Badge, Button, DeleteActionModal, DropdownMenu, @@ -20,6 +21,7 @@ import { TBody, Td, THead, + Tooltip, Tr } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; @@ -27,14 +29,31 @@ import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { fetchSlackReinstallUrl, + useCheckMicrosoftTeamsIntegrationInstallationStatus, + useDeleteMicrosoftTeamsIntegration, useDeleteSlackIntegration, useGetWorkflowIntegrations } from "@app/hooks/api"; -import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; +import { + WorkflowIntegrationPlatform, + WorkflowIntegrationStatus +} from "@app/hooks/api/workflowIntegrations/types"; import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm"; import { IntegrationFormDetails } from "./IntegrationFormDetails"; +const renderStatus = (status: WorkflowIntegrationStatus) => { + if (status === WorkflowIntegrationStatus.Installed) { + return Installed; + } + + if (status === WorkflowIntegrationStatus.Pending) { + return Pending; + } + + return Failed; +}; + export const OrgWorkflowIntegrationTab = withPermission( () => { const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ @@ -48,6 +67,9 @@ export const OrgWorkflowIntegrationTab = withPermission( useGetWorkflowIntegrations(currentOrg?.id); const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); + const { mutateAsync: deleteMicrosoftTeamsIntegration } = useDeleteMicrosoftTeamsIntegration(); + const { mutateAsync: checkMicrosoftTeamsInstallationStatus } = + useCheckMicrosoftTeamsIntegrationInstallationStatus(); const handleRemoveIntegration = async () => { if (!currentOrg) { @@ -62,6 +84,13 @@ export const OrgWorkflowIntegrationTab = withPermission( }); } + if (platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) { + await deleteMicrosoftTeamsIntegration({ + id, + orgId: currentOrg?.id + }); + } + handlePopUpClose("removeIntegration"); createNotification({ text: "Successfully deleted integration", @@ -69,27 +98,37 @@ export const OrgWorkflowIntegrationTab = withPermission( }); }; - const triggerReinstall = async (platform: WorkflowIntegrationPlatform, id: string) => { - if (platform === WorkflowIntegrationPlatform.SLACK) { - try { - const slackReinstallUrl = await fetchSlackReinstallUrl({ - id - }); + const triggerSlackReinstall = async (id: string) => { + try { + const slackReinstallUrl = await fetchSlackReinstallUrl({ + id + }); - if (slackReinstallUrl) { - window.location.href = slackReinstallUrl; - } - } catch (err) { - if (axios.isAxiosError(err)) { - createNotification({ - text: (err.response?.data as { message: string })?.message, - type: "error" - }); - } + if (slackReinstallUrl) { + window.location.href = slackReinstallUrl; + } + } catch (err) { + if (axios.isAxiosError(err)) { + createNotification({ + text: (err.response?.data as { message: string })?.message, + type: "error" + }); } } }; + const triggerMicrosoftTeamsInstallationStatusCheck = async (id: string) => { + await checkMicrosoftTeamsInstallationStatus({ + workflowIntegrationId: id, + orgId: currentOrg?.id + }); + + createNotification({ + text: "The Microsoft Teams bot is successfully installed in your Microsoft Teams tenant", + type: "success" + }); + }; + return (
@@ -117,6 +156,7 @@ export const OrgWorkflowIntegrationTab = withPermission( Provider Alias + Status @@ -134,11 +174,23 @@ export const OrgWorkflowIntegrationTab = withPermission( )} {workflowIntegrations?.map((workflowIntegration) => ( - - -
{workflowIntegration.integration.toUpperCase()}
+ + {workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK ? ( + + ) : ( + + )} +
+ {workflowIntegration.integration.replaceAll("-", " ")} +
+ {workflowIntegration.description && ( + + + + )} {workflowIntegration.slug} + {renderStatus(workflowIntegration.status)} @@ -159,29 +211,56 @@ export const OrgWorkflowIntegrationTab = withPermission( > More details - - {(isAllowed) => ( - { - e.stopPropagation(); - triggerReinstall( - workflowIntegration.integration, - workflowIntegration.id - ); - }} - > - Reinstall - - )} - + {workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK && ( + + {(isAllowed) => ( + { + e.stopPropagation(); + + await triggerSlackReinstall(workflowIntegration.id); + }} + > + Reinstall + + )} + + )} + + {workflowIntegration.integration === + WorkflowIntegrationPlatform.MICROSOFT_TEAMS && ( + + {(isAllowed) => ( + { + e.stopPropagation(); + + await triggerMicrosoftTeamsInstallationStatusCheck( + workflowIntegration.id + ); + }} + > + Check Installation Status + + )} + + )} + ; +export const renderProvider = (integration: WorkflowIntegrationPlatform) => { + if (integration === WorkflowIntegrationPlatform.SLACK) { + return ; + } + + if (integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) { + return ; + } + + return null; +}; export const WorkflowIntegrationTab = () => { + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "addWorkflowIntegration", + "removeIntegration", + "editIntegration" + ] as const); + const { currentWorkspace } = useWorkspace(); - const { data: slackConfig, isPending: isSlackConfigLoading } = useGetWorkspaceSlackConfig({ - workspaceId: currentWorkspace?.id ?? "" - }); - const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId); - const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig(); + const { data: slackConfig, isPending: isSlackConfigLoading } = + useGetWorkspaceWorkflowIntegrationConfig({ + workspaceId: currentWorkspace?.id ?? "", + integration: WorkflowIntegrationPlatform.SLACK + }); - const { - control, - watch, - handleSubmit, - setValue, - formState: { isDirty, isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - isAccessRequestNotificationEnabled: false, - accessRequestChannels: [], - isSecretRequestNotificationEnabled: false, - secretRequestChannels: [] - } - }); + const { data: microsoftTeamsConfig, isPending: isMicrosoftTeamsConfigLoading } = + useGetWorkspaceWorkflowIntegrationConfig({ + workspaceId: currentWorkspace?.id ?? "", + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS + }); - const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); - const selectedSlackIntegrationId = watch("slackIntegrationId"); - const accessRequestNotifState = watch("isAccessRequestNotificationEnabled"); + const { mutateAsync: deleteIntegration } = useDeleteProjectWorkflowIntegration(); - const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId); - const slackChannelIdToName = Object.fromEntries( - (slackChannels || []).map((channel) => [channel.id, channel.name]) - ); - const sortedSlackChannels = slackChannels?.sort((a, b) => - a.name.toLowerCase().localeCompare(b.name.toLowerCase()) - ); - - const handleIntegrationSave = async (data: TSlackConfigForm) => { - if (!currentWorkspace) { + const handleRemoveIntegration = async ( + integrationType: WorkflowIntegrationPlatform, + integrationId: string + ) => { + if (!currentWorkspace.id) { return; } - await updateProjectSlackConfig({ - workspaceId: currentWorkspace.id, - ...data, - accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), - secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ") + await deleteIntegration({ + projectId: currentWorkspace?.id ?? "", + integration: integrationType, + integrationId }); createNotification({ type: "success", - text: "Successfully updated slack integration" + text: `Successfully removed ${integrationType.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase())} integration` }); }; - useEffect(() => { - if (slackConfig) { - setValue("slackIntegrationId", slackConfig.slackIntegrationId); - setValue( - "isSecretRequestNotificationEnabled", - slackConfig.isSecretRequestNotificationEnabled - ); - setValue( - "isAccessRequestNotificationEnabled", - slackConfig.isAccessRequestNotificationEnabled - ); - - if (slackChannels) { - setValue( - "secretRequestChannels", - slackConfig.secretRequestChannels - .split(", ") - .filter((channel) => channel in slackChannelIdToName) - ); - setValue( - "accessRequestChannels", - slackConfig.accessRequestChannels - .split(", ") - .filter((channel) => channel in slackChannelIdToName) - ); - } - } - }, [slackConfig, slackChannels]); - - if (isSlackConfigLoading) { - return ; - } - - return !slackIntegrations?.length ? ( - - -
- Create one now -
- -
- ) : ( + return (
-

Slack Integration

+

Workflow Integrations

+ + {(isAllowed) => ( + + )} +

- This integration allows you to send notifications to your Slack workspace in response to - events in your project. + Connect Infisical to other platforms for notification and workflow integrations.

-
-
- - {(isAllowed) => ( - ( - - - - )} - control={control} - name="slackIntegrationId" + + {!!slackConfig || !!microsoftTeamsConfig ? ( + + + + + + + + + + - )} - - - {selectedSlackIntegrationId && ( - <> -

Events

- { - return ( - - field.onChange(value)} - isChecked={field.value} - > -

Secret Approval Requests

-
-
- ); - }} + +
+
ProviderAccess Request Notifications DestinationSecret Request Notifications Destination +
+ ) : ( +
+ - {secretRequestNotifState && ( - ( - - - - slackChannelIdToName[entry]) - .join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`secret-requests-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - - - )} - /> - )} - { - return ( - - field.onChange(value)} - isChecked={field.value} - > -

Access Requests

-
-
- ); - }} - /> - {accessRequestNotifState && ( - ( - - - - slackChannelIdToName[entry]) - .join(", ")} - className="text-left" - /> - - - {sortedSlackChannels?.map((slackChannel) => { - const isChecked = value?.includes(slackChannel.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el: string) => el !== slackChannel.id) - : [...(value || []), slackChannel.id] - ); - }} - key={`access-requests-slack-channel-${slackChannel.id}`} - iconPos="right" - icon={isChecked && } - > - {slackChannel.name} - - ); - })} - - - - )} - /> - )} - - +
)} - +
+ handlePopUpToggle("addWorkflowIntegration", state)} + /> + handlePopUpToggle("removeIntegration", isOpen)} + deleteKey="confirm" + onDeleteApproved={async () => { + await handleRemoveIntegration( + popUp.removeIntegration.data?.integration, + popUp.removeIntegration.data?.integrationId + ); + handlePopUpClose("removeIntegration"); + }} + /> + handlePopUpClose("editIntegration")} + integration={popUp.editIntegration.data?.integration} + />
); }; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/AddWorkflowIntegrationModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/AddWorkflowIntegrationModal.tsx new file mode 100644 index 000000000..8601c1c02 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/AddWorkflowIntegrationModal.tsx @@ -0,0 +1,153 @@ +import { useState } from "react"; +import { BsMicrosoftTeams } from "react-icons/bs"; +import { faSlack } from "@fortawesome/free-brands-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AnimatePresence, motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useGetWorkspaceWorkflowIntegrationConfig } from "@app/hooks/api"; +import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; + +import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm"; +import { SlackIntegrationForm } from "./SlackIntegrationForm"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; +}; + +enum WizardSteps { + SelectPlatform = "select-platform", + PlatformInputs = "platform-inputs" +} + +const PLATFORM_LIST = [ + { + icon: , + platform: WorkflowIntegrationPlatform.SLACK, + title: "Slack" + }, + { + icon: , + platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS, + title: "Microsoft Teams" + } +]; + +export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => { + const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform); + const [selectedPlatform, setSelectedPlatform] = useState(null); + + const { currentWorkspace } = useWorkspace(); + const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({ + workspaceId: currentWorkspace?.id ?? "", + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS + }); + + const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({ + workspaceId: currentWorkspace?.id ?? "", + integration: WorkflowIntegrationPlatform.SLACK + }); + + const microsoftTeamsConfigured = !!microsoftTeamsConfig; + const slackConfigured = !!slackConfig; + + const handleFormReset = (state: boolean = false) => { + onToggle(state); + setWizardStep(WizardSteps.SelectPlatform); + setSelectedPlatform(null); + }; + + return ( + handleFormReset(state)}> + char.toUpperCase()) ?? "workflow"} integration`} + className="my-4" + > + + {wizardStep === WizardSteps.SelectPlatform && ( + +
Select a workflow integration
+
+ {PLATFORM_LIST.map(({ icon, platform, title }) => { + const isConfigured = + (platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && + microsoftTeamsConfigured) || + (platform === WorkflowIntegrationPlatform.SLACK && slackConfigured); + + return ( +
+
{ + setSelectedPlatform(platform); + setWizardStep(WizardSteps.PlatformInputs); + }} + onKeyDown={(evt) => { + if (evt.key === "Enter") { + setSelectedPlatform(platform); + setWizardStep(WizardSteps.PlatformInputs); + } + }} + > +
{icon}
+
{title}
+
+ {isConfigured && ( +
+
+
+ Already Configured +
+
+
+ )} +
+ ); + })} +
+
+ )} + + {wizardStep === WizardSteps.PlatformInputs && + selectedPlatform === WorkflowIntegrationPlatform.SLACK && ( + + handleFormReset(false)} /> + + )} + {wizardStep === WizardSteps.PlatformInputs && + selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && ( + + handleFormReset(false)} /> + + )} +
+
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/EditWorkflowIntegrationModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/EditWorkflowIntegrationModal.tsx new file mode 100644 index 000000000..1cf727500 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/EditWorkflowIntegrationModal.tsx @@ -0,0 +1,33 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; + +import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm"; +import { SlackIntegrationForm } from "./SlackIntegrationForm"; + +type Props = { + isOpen?: boolean; + onClose: () => void; + + integration: WorkflowIntegrationPlatform; +}; + +export const EditWorkflowIntegrationModal = ({ isOpen, onClose, integration }: Props) => { + const handleFormReset = () => { + onClose(); + }; + + return ( + + char.toUpperCase()) ?? "workflow"} integration`} + > + {integration === WorkflowIntegrationPlatform.SLACK && ( + + )} + {integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && ( + + )} + + + ); +}; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx new file mode 100644 index 000000000..ef7617d52 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsConfigRow.tsx @@ -0,0 +1,148 @@ +/* eslint-disable no-nested-ternary */ +import { BsMicrosoftTeams } from "react-icons/bs"; +import { faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Spinner, + Td, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useGetMicrosoftTeamsIntegrationTeams } from "@app/hooks/api"; +import { + ProjectWorkflowIntegrationConfig, + WorkflowIntegrationPlatform +} from "@app/hooks/api/workflowIntegrations/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + microsoftTeamsConfig?: ProjectWorkflowIntegrationConfig | null; + isMicrosoftTeamsConfigLoading: boolean; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>, + data?: { + integrationId?: string; + integration: WorkflowIntegrationPlatform; + } + ) => void; +}; + +export const MicrosoftTeamsConfigRow = ({ + handlePopUpOpen, + isMicrosoftTeamsConfigLoading, + microsoftTeamsConfig +}: Props) => { + const { data: microsoftTeamsChannels, isPending: isMicrosoftTeamsChannelsLoading } = + useGetMicrosoftTeamsIntegrationTeams(microsoftTeamsConfig?.integrationId); + const microsoftTeamsChannelIdToName = Object.fromEntries( + microsoftTeamsChannels?.flatMap((team) => + team.channels.map((channel) => [channel.channelId, channel.channelName]) + ) ?? [] + ); + + if (microsoftTeamsConfig?.integration !== WorkflowIntegrationPlatform.MICROSOFT_TEAMS) { + return null; + } + + const isLoadingConfig = isMicrosoftTeamsChannelsLoading || isMicrosoftTeamsConfigLoading; + + return ( + + +
+ + Microsoft Teams +
+ + + {microsoftTeamsConfig.isAccessRequestNotificationEnabled && + !isLoadingConfig && + microsoftTeamsConfig.accessRequestChannels?.channelIds?.length > 0 ? ( + + {microsoftTeamsConfig.accessRequestChannels.channelIds + .map((channel) => microsoftTeamsChannelIdToName[channel]) + .join(", ")} + + ) : isLoadingConfig ? ( + + ) : ( + Disabled + )} + + + {microsoftTeamsConfig.isSecretRequestNotificationEnabled && + !isLoadingConfig && + microsoftTeamsConfig.secretRequestChannels?.channelIds?.length > 0 ? ( + + {microsoftTeamsConfig.secretRequestChannels.channelIds + .map((channel) => microsoftTeamsChannelIdToName[channel]) + .join(", ")} + + ) : isLoadingConfig ? ( + + ) : ( + Disabled + )} + + + + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + + handlePopUpOpen("editIntegration", { + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS + }); + }} + > + Edit + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + + handlePopUpOpen("removeIntegration", { + integrationId: microsoftTeamsConfig.integrationId, + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS + }); + }} + > + Delete + + )} + + +
+ + + ); +}; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx new file mode 100644 index 000000000..aab932b8d --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/MicrosoftTeamsIntegrationForm.tsx @@ -0,0 +1,556 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + FormControl, + Input, + Select, + SelectItem, + Switch +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext"; +import { + useGetMicrosoftTeamsIntegrations, + useGetMicrosoftTeamsIntegrationTeams, + useGetWorkspaceWorkflowIntegrationConfig, + useUpdateProjectWorkflowIntegrationConfig +} from "@app/hooks/api"; +import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; + +const formSchema = z + .object({ + microsoftTeamsIntegrationId: z.string(), + isSecretRequestNotificationEnabled: z.boolean(), + isAccessRequestNotificationEnabled: z.boolean(), + secretRequestChannels: z + .object({ + teamId: z.string(), + channelIds: z.string().array() + }) + .optional(), + accessRequestChannels: z + .object({ + teamId: z.string(), + channelIds: z.string().array() + }) + .optional() + }) + .superRefine((data, ctx) => { + if (data.isSecretRequestNotificationEnabled) { + if (!data?.secretRequestChannels?.teamId) { + ctx.addIssue({ + path: ["secretRequestChannels", "teamId"], + code: z.ZodIssueCode.custom, + message: "Team is required" + }); + } + + if (!data?.secretRequestChannels?.channelIds?.length) { + ctx.addIssue({ + path: ["secretRequestChannels", "channelIds"], + code: z.ZodIssueCode.custom, + message: "At least one channel is required" + }); + } + } + + if (data.isAccessRequestNotificationEnabled) { + if (!data?.accessRequestChannels?.teamId) { + ctx.addIssue({ + path: ["accessRequestChannels", "teamId"], + code: z.ZodIssueCode.custom, + message: "Team is required" + }); + } + + if (!data?.accessRequestChannels?.channelIds?.length) { + ctx.addIssue({ + path: ["accessRequestChannels", "channelIds"], + code: z.ZodIssueCode.custom, + message: "At least one channel is required" + }); + } + } + }); + +type TMicrosoftTeamsConfigForm = z.infer; + +type Props = { + onClose: () => void; +}; + +export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({ + workspaceId: currentWorkspace?.id ?? "", + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS + }); + const { data: microsoftTeamsIntegrations } = useGetMicrosoftTeamsIntegrations( + currentWorkspace?.orgId + ); + + const { mutateAsync: updateProjectMicrosoftTeamsConfig } = + useUpdateProjectWorkflowIntegrationConfig(); + + const { + control, + watch, + handleSubmit, + setValue, + formState: { isDirty, isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + isAccessRequestNotificationEnabled: false, + isSecretRequestNotificationEnabled: false, + accessRequestChannels: { + teamId: "", + channelIds: [] + }, + secretRequestChannels: { + teamId: "", + channelIds: [] + } + } + }); + + const handleIntegrationSave = async (data: TMicrosoftTeamsConfigForm) => { + try { + if (!currentWorkspace) { + return; + } + + await updateProjectMicrosoftTeamsConfig({ + workspaceId: currentWorkspace.id, + isAccessRequestNotificationEnabled: data.isAccessRequestNotificationEnabled, + isSecretRequestNotificationEnabled: data.isSecretRequestNotificationEnabled, + ...(data.isAccessRequestNotificationEnabled && { + accessRequestChannels: data.accessRequestChannels + }), + ...(data.isSecretRequestNotificationEnabled && { + secretRequestChannels: data.secretRequestChannels + }), + integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS, + integrationId: data.microsoftTeamsIntegrationId + }); + + createNotification({ + type: "success", + text: "Successfully created microsoft teams integration" + }); + + onClose(); + } catch { + createNotification({ + type: "error", + text: "Failed to create microsoft teams integration" + }); + } + }; + + const selectedAccessRequestTeamId = watch("accessRequestChannels.teamId"); + const selectedSecretRequestTeamId = watch("secretRequestChannels.teamId"); + + const selectedMicrosoftTeamsIntegrationId = watch("microsoftTeamsIntegrationId"); + + const accessRequestNotificationsEnabled = watch("isAccessRequestNotificationEnabled"); + const secretRequestNotificationsEnabled = watch("isSecretRequestNotificationEnabled"); + + const { + data: microsoftTeamsIntegrationTeams, + isPending: isLoadingMicrosoftTeamsIntegrationTeams + } = useGetMicrosoftTeamsIntegrationTeams(selectedMicrosoftTeamsIntegrationId); + + const sortedMicrosoftTeamsIntegrationTeams = microsoftTeamsIntegrationTeams?.sort((a, b) => + a.teamName.toLowerCase().localeCompare(b.teamName.toLowerCase()) + ); + + const selectableAccessRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams + ?.filter((team) => team.teamId === selectedAccessRequestTeamId) + .map((team) => team.channels) + .flat(); + const selectableSecretRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams + ?.filter((team) => team.teamId === selectedSecretRequestTeamId) + .map((team) => team.channels) + .flat(); + + const channelIdToName = [ + ...(selectableAccessRequestChannelIds || []), + ...(selectableSecretRequestChannelIds || []) + ].reduce( + (acc, channel) => { + acc[channel.channelId] = channel.channelName; + return acc; + }, + {} as Record + ); + + useEffect(() => { + if (microsoftTeamsConfig) { + setValue("microsoftTeamsIntegrationId", microsoftTeamsConfig.integrationId); + setValue( + "isSecretRequestNotificationEnabled", + microsoftTeamsConfig.isSecretRequestNotificationEnabled + ); + setValue( + "isAccessRequestNotificationEnabled", + microsoftTeamsConfig.isAccessRequestNotificationEnabled + ); + + if (microsoftTeamsConfig.integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) { + if (microsoftTeamsConfig.secretRequestChannels) { + if (Object.entries(microsoftTeamsConfig.accessRequestChannels).length) { + setValue("accessRequestChannels", microsoftTeamsConfig.accessRequestChannels); + } + if (Object.entries(microsoftTeamsConfig.secretRequestChannels).length) { + setValue("secretRequestChannels", microsoftTeamsConfig.secretRequestChannels); + } + } + } + } + }, [microsoftTeamsConfig]); + + return ( +
+
+ + {(isAllowed) => ( + ( + + + + )} + /> + )} + +
+ {selectedMicrosoftTeamsIntegrationId && ( + <> +

Configure Events

+ { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Secret Approval Requests

+
+
+ ); + }} + /> + {secretRequestNotificationsEnabled && ( + <> + ( + + + + )} + /> + ( + + + + {selectedSecretRequestTeamId ? ( + channelIdToName[entry]) + .join(", ") + } + className="text-left" + /> + ) : ( + + )} + + + {selectableSecretRequestChannelIds?.map((channel) => { + const isChecked = value?.includes(channel.channelId); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== channel.channelId) + : [...(value || []), channel.channelId] + ); + }} + key={`secret-requests-microsoft-teams-channel-${channel.channelId}`} + iconPos="right" + icon={isChecked && } + > + {channel.channelName} + + ); + })} + + + + )} + /> + + )} + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Access Requests

+
+
+ ); + }} + /> + {accessRequestNotificationsEnabled && ( + <> + ( + + + + )} + /> + + ( + + + + {selectedAccessRequestTeamId ? ( + channelIdToName[entry]) + .join(", ") + } + className="text-left" + /> + ) : ( + + )} + + + {selectableAccessRequestChannelIds?.map((channel) => { + const isChecked = value?.includes(channel.channelId); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== channel.channelId) + : [...(value || []), channel.channelId] + ); + }} + key={`access-requests-slack-channel-${channel.channelId}`} + iconPos="right" + icon={isChecked && } + > + {channel.channelName} + + ); + })} + + + + )} + /> + + )} + + + )} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx new file mode 100644 index 000000000..df7211f99 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackConfigRow.tsx @@ -0,0 +1,145 @@ +/* eslint-disable no-nested-ternary */ +import { BsSlack } from "react-icons/bs"; +import { faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Spinner, + Td, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useGetSlackIntegrationChannels } from "@app/hooks/api"; +import { + ProjectWorkflowIntegrationConfig, + WorkflowIntegrationPlatform +} from "@app/hooks/api/workflowIntegrations/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + slackConfig?: ProjectWorkflowIntegrationConfig | null; + isSlackConfigLoading: boolean; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>, + data?: { + integrationId?: string; + integration: WorkflowIntegrationPlatform; + } + ) => void; +}; + +export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackConfig }: Props) => { + const { data: slackChannels, isPending: isSlackChannelsLoading } = useGetSlackIntegrationChannels( + slackConfig?.integrationId + ); + const slackChannelIdToName = Object.fromEntries( + (slackChannels || []).map((channel) => [channel.id, channel.name]) + ); + + if (slackConfig?.integration !== WorkflowIntegrationPlatform.SLACK) { + return null; + } + + const isLoadingConfig = isSlackChannelsLoading || isSlackConfigLoading; + + return ( + + +
+ + Slack +
+ + + {slackConfig.isAccessRequestNotificationEnabled && + !isLoadingConfig && + slackConfig.accessRequestChannels.length > 0 ? ( + + {slackConfig.accessRequestChannels + .split(", ") + .map((channel) => slackChannelIdToName[channel]) + .join(", ")} + + ) : isLoadingConfig ? ( + + ) : ( + Disabled + )} + + + {slackConfig.isSecretRequestNotificationEnabled && + !isLoadingConfig && + slackConfig.secretRequestChannels.length > 0 ? ( + + {slackConfig.secretRequestChannels + .split(", ") + .map((channel) => slackChannelIdToName[channel]) + .join(", ")} + + ) : isLoadingConfig ? ( + + ) : ( + Disabled + )} + + + + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + + handlePopUpOpen("editIntegration", { + integration: WorkflowIntegrationPlatform.SLACK + }); + }} + > + Edit + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + + handlePopUpOpen("removeIntegration", { + integrationId: slackConfig.integrationId, + integration: WorkflowIntegrationPlatform.SLACK + }); + }} + > + Delete + + )} + + +
+ + + ); +}; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx new file mode 100644 index 000000000..6bcaa6ac1 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/components/SlackIntegrationForm.tsx @@ -0,0 +1,355 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + FormControl, + Input, + Select, + SelectItem, + Switch +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext"; +import { + useGetSlackIntegrationChannels, + useGetWorkflowIntegrations, + useGetWorkspaceWorkflowIntegrationConfig, + useUpdateProjectWorkflowIntegrationConfig +} from "@app/hooks/api"; +import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; + +const formSchema = z.object({ + slackIntegrationId: z.string(), + isSecretRequestNotificationEnabled: z.boolean(), + secretRequestChannels: z.string().array(), + isAccessRequestNotificationEnabled: z.boolean(), + accessRequestChannels: z.string().array() +}); + +type TSlackConfigForm = z.infer; + +type Props = { + onClose: () => void; +}; + +export const SlackIntegrationForm = ({ onClose }: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({ + workspaceId: currentWorkspace?.id ?? "", + integration: WorkflowIntegrationPlatform.SLACK + }); + const { data: workflowIntegrations } = useGetWorkflowIntegrations(currentWorkspace?.orgId); + const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectWorkflowIntegrationConfig(); + + const slackIntegrations = workflowIntegrations?.filter( + (integration) => integration.integration === WorkflowIntegrationPlatform.SLACK + ); + + const { + control, + watch, + handleSubmit, + setValue, + formState: { isDirty, isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + isAccessRequestNotificationEnabled: false, + accessRequestChannels: [], + isSecretRequestNotificationEnabled: false, + secretRequestChannels: [] + } + }); + + const handleIntegrationSave = async (data: TSlackConfigForm) => { + try { + if (!currentWorkspace) { + return; + } + + await updateProjectSlackConfig({ + ...data, + workspaceId: currentWorkspace.id, + integration: WorkflowIntegrationPlatform.SLACK, + integrationId: data.slackIntegrationId, + accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), + secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ") + }); + + createNotification({ + type: "success", + text: "Successfully created slack integration" + }); + + onClose(); + } catch { + createNotification({ + type: "error", + text: "Failed to create slack integration" + }); + } + }; + + const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); + const selectedSlackIntegrationId = watch("slackIntegrationId"); + const accessRequestNotifState = watch("isAccessRequestNotificationEnabled"); + + const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId); + const slackChannelIdToName = Object.fromEntries( + (slackChannels || []).map((channel) => [channel.id, channel.name]) + ); + const sortedSlackChannels = slackChannels?.sort((a, b) => + a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + ); + + useEffect(() => { + if (slackConfig) { + setValue("slackIntegrationId", slackConfig.integrationId); + setValue( + "isSecretRequestNotificationEnabled", + slackConfig.isSecretRequestNotificationEnabled + ); + setValue( + "isAccessRequestNotificationEnabled", + slackConfig.isAccessRequestNotificationEnabled + ); + + if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) { + if (slackChannels) { + setValue( + "secretRequestChannels", + slackConfig.secretRequestChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); + setValue( + "accessRequestChannels", + slackConfig.accessRequestChannels + .split(", ") + .filter((channel) => channel in slackChannelIdToName) + ); + } + } + } + }, [slackConfig, slackChannels]); + + return ( +
+
+ + {(isAllowed) => ( + ( + + + + )} + /> + )} + +
+ {selectedSlackIntegrationId && ( + <> +

Configure Events

+ { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Secret Approval Requests

+
+
+ ); + }} + /> + {secretRequestNotifState && ( + ( + + + + slackChannelIdToName[entry]) + .join(", ")} + className="text-left" + /> + + + {sortedSlackChannels?.map((slackChannel) => { + const isChecked = value?.includes(slackChannel.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== slackChannel.id) + : [...(value || []), slackChannel.id] + ); + }} + key={`secret-requests-slack-channel-${slackChannel.id}`} + iconPos="right" + icon={isChecked && } + > + {slackChannel.name} + + ); + })} + + + + )} + /> + )} + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Access Requests

+
+
+ ); + }} + /> + {accessRequestNotifState && ( + ( + + + + slackChannelIdToName[entry]) + .join(", ")} + className="text-left" + /> + + + {sortedSlackChannels?.map((slackChannel) => { + const isChecked = value?.includes(slackChannel.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el: string) => el !== slackChannel.id) + : [...(value || []), slackChannel.id] + ); + }} + key={`access-requests-slack-channel-${slackChannel.id}`} + iconPos="right" + icon={isChecked && } + > + {slackChannel.name} + + ); + })} + + + + )} + /> + )} + + + )} + + ); +}; diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 69186b45d..2047dc070 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -42,7 +42,6 @@ import { Route as adminLayoutImport } from './pages/admin/layout' import { Route as authProviderSuccessPageRouteImport } from './pages/auth/ProviderSuccessPage/route' import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route' import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route' -import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route' import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' @@ -62,6 +61,7 @@ import { Route as organizationKmsOverviewPageRouteImport } from './pages/organiz import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route' import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route' import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route' +import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route' import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route' import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route' import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route' @@ -87,6 +87,7 @@ import { Route as secretManagerSecretRotationPageRouteImport } from './pages/sec import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route' import { Route as secretManagerSecretApprovalsPageRouteImport } from './pages/secret-manager/SecretApprovalsPage/route' import { Route as secretManagerIPAllowlistPageRouteImport } from './pages/secret-manager/IPAllowlistPage/route' +import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from './pages/organization/SettingsPage/OauthCallbackPage/route' import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/route' import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route' import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route' @@ -217,6 +218,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport = createFileRoute( '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId', )() +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport = + createFileRoute( + '/_authenticate/_inject-org-details/_org-layout/organization/settings', + )() const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport = createFileRoute( '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing', @@ -483,6 +488,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute = getParentRoute: () => organizationLayoutRoute, } as any) +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute = + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, + } as any) + const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport.update({ id: '/secret-sharing', @@ -521,14 +534,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRoute = getParentRoute: () => organizationLayoutRoute, } as any) -const organizationSettingsPageRouteRoute = - organizationSettingsPageRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, - } as any) - const organizationSecretScanningPageRouteRoute = organizationSecretScanningPageRouteImport.update({ id: '/secret-scanning', @@ -670,6 +675,14 @@ const organizationCertManagerOverviewPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) +const organizationSettingsPageRouteRoute = + organizationSettingsPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute, + } as any) + const organizationSecretSharingPageRouteRoute = organizationSecretSharingPageRouteImport.update({ id: '/', @@ -864,6 +877,14 @@ const secretManagerIPAllowlistPageRouteRoute = getParentRoute: () => secretManagerLayoutRoute, } as any) +const organizationSettingsPageOauthCallbackPageRouteRoute = + organizationSettingsPageOauthCallbackPageRouteImport.update({ + id: '/oauth/callback', + path: '/oauth/callback', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute, + } as any) + const kmsSettingsPageRouteRoute = kmsSettingsPageRouteImport.update({ id: '/settings', path: '/settings', @@ -1952,13 +1973,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationSecretScanningPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } - '/_authenticate/_inject-org-details/_org-layout/organization/settings': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/settings' - path: '/settings' - fullPath: '/organization/settings' - preLoaderRoute: typeof organizationSettingsPageRouteImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport - } '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' path: '/cert-manager/$projectId' @@ -1994,6 +2008,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/settings': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/settings' + path: '/settings' + fullPath: '/organization/settings' + preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId' path: '/secret-manager/$projectId' @@ -2029,6 +2050,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationSecretSharingPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport } + '/_authenticate/_inject-org-details/_org-layout/organization/settings/': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/settings/' + path: '/' + fullPath: '/organization/settings/' + preLoaderRoute: typeof organizationSettingsPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport + } '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': { id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview' path: '/cert-manager/overview' @@ -2169,6 +2197,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof kmsSettingsPageRouteImport parentRoute: typeof kmsLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback' + path: '/oauth/callback' + fullPath: '/organization/settings/oauth/callback' + preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport + } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist' path: '/allowlist' @@ -3090,16 +3125,33 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChi AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren, ) +interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren { + organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute + organizationSettingsPageOauthCallbackPageRouteRoute: typeof organizationSettingsPageOauthCallbackPageRouteRoute +} + +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren = + { + organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute, + organizationSettingsPageOauthCallbackPageRouteRoute: + organizationSettingsPageOauthCallbackPageRouteRoute, + } + +const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren = + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute._addFileChildren( + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteChildren, + ) + interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute - organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute @@ -3119,13 +3171,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, organizationSecretScanningPageRouteRoute: organizationSecretScanningPageRouteRoute, - organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren, + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren, organizationCertManagerOverviewPageRouteRoute: organizationCertManagerOverviewPageRouteRoute, organizationGroupDetailsByIDPageRouteRoute: @@ -3809,17 +3862,18 @@ export interface FileRoutesByFullPath { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute - '/organization/settings': typeof organizationSettingsPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren '/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren '/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren + '/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren '/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren '/ssh/$projectId': typeof sshLayoutRouteWithChildren '/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute '/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute + '/organization/settings/': typeof organizationSettingsPageRouteRoute '/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute @@ -3836,6 +3890,7 @@ export interface FileRoutesByFullPath { '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute '/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute + '/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute '/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute '/secret-manager/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute '/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute @@ -3989,7 +4044,6 @@ export interface FileRoutesByTo { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute - '/organization/settings': typeof organizationSettingsPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren '/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren @@ -3997,6 +4051,7 @@ export interface FileRoutesByTo { '/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute + '/organization/settings': typeof organizationSettingsPageRouteRoute '/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute @@ -4013,6 +4068,7 @@ export interface FileRoutesByTo { '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute '/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute + '/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute '/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute '/secret-manager/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute '/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute @@ -4175,17 +4231,18 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof organizationSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren + '/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute @@ -4206,6 +4263,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings': typeof kmsSettingsPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist': typeof secretManagerIPAllowlistPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval': typeof secretManagerSecretApprovalsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute @@ -4365,17 +4423,18 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/secret-scanning' - | '/organization/settings' | '/cert-manager/$projectId' | '/kms/$projectId' | '/organization/app-connections' | '/organization/gateways' | '/organization/secret-sharing' + | '/organization/settings' | '/secret-manager/$projectId' | '/ssh/$projectId' | '/organization/app-connections/' | '/organization/gateways/' | '/organization/secret-sharing/' + | '/organization/settings/' | '/organization/cert-manager/overview' | '/organization/groups/$groupId' | '/organization/identities/$identityId' @@ -4392,6 +4451,7 @@ export interface FileRouteTypes { | '/kms/$projectId/kmip' | '/kms/$projectId/overview' | '/kms/$projectId/settings' + | '/organization/settings/oauth/callback' | '/secret-manager/$projectId/allowlist' | '/secret-manager/$projectId/approval' | '/secret-manager/$projectId/overview' @@ -4544,7 +4604,6 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/secret-scanning' - | '/organization/settings' | '/cert-manager/$projectId' | '/kms/$projectId' | '/secret-manager/$projectId' @@ -4552,6 +4611,7 @@ export interface FileRouteTypes { | '/organization/app-connections' | '/organization/gateways' | '/organization/secret-sharing' + | '/organization/settings' | '/organization/cert-manager/overview' | '/organization/groups/$groupId' | '/organization/identities/$identityId' @@ -4568,6 +4628,7 @@ export interface FileRouteTypes { | '/kms/$projectId/kmip' | '/kms/$projectId/overview' | '/kms/$projectId/settings' + | '/organization/settings/oauth/callback' | '/secret-manager/$projectId/allowlist' | '/secret-manager/$projectId/approval' | '/secret-manager/$projectId/overview' @@ -4728,17 +4789,18 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/billing' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning' - | '/_authenticate/_inject-org-details/_org-layout/organization/settings' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections' | '/_authenticate/_inject-org-details/_org-layout/organization/gateways' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing' + | '/_authenticate/_inject-org-details/_org-layout/organization/settings' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/' | '/_authenticate/_inject-org-details/_org-layout/organization/gateways/' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/' + | '/_authenticate/_inject-org-details/_org-layout/organization/settings/' | '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview' | '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId' @@ -4759,6 +4821,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings' + | '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview' @@ -5117,10 +5180,10 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs", "/_authenticate/_inject-org-details/_org-layout/organization/billing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning", - "/_authenticate/_inject-org-details/_org-layout/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections", "/_authenticate/_inject-org-details/_org-layout/organization/gateways", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", + "/_authenticate/_inject-org-details/_org-layout/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview", "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId", @@ -5162,10 +5225,6 @@ export const routeTree = rootRoute "filePath": "organization/SecretScanningPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, - "/_authenticate/_inject-org-details/_org-layout/organization/settings": { - "filePath": "organization/SettingsPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization" - }, "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout", @@ -5203,6 +5262,14 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" ] }, + "/_authenticate/_inject-org-details/_org-layout/organization/settings": { + "filePath": "", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization", + "children": [ + "/_authenticate/_inject-org-details/_org-layout/organization/settings/", + "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" + ] + }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout", @@ -5229,6 +5296,10 @@ export const routeTree = rootRoute "filePath": "organization/SecretSharingPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" }, + "/_authenticate/_inject-org-details/_org-layout/organization/settings/": { + "filePath": "organization/SettingsPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings" + }, "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": { "filePath": "organization/CertManagerOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" @@ -5354,6 +5425,10 @@ export const routeTree = rootRoute "filePath": "kms/SettingsPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" }, + "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback": { + "filePath": "organization/SettingsPage/OauthCallbackPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings" + }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/allowlist": { "filePath": "secret-manager/IPAllowlistPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 6c5c2832e..4bfb82fad 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -20,7 +20,10 @@ const organizationRoutes = route("/organization", [ index("organization/SecretSharingPage/route.tsx"), route("/settings", "organization/SecretSharingSettingsPage/route.tsx") ]), - route("/settings", "organization/SettingsPage/route.tsx"), + route("/settings", [ + index("organization/SettingsPage/route.tsx"), + route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx") + ]), route("/secret-scanning", "organization/SecretScanningPage/route.tsx"), route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"), route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"),