diff --git a/.infisicalignore b/.infisicalignore index 85883e646..b00bf0995 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -25,3 +25,6 @@ frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationL docs/documentation/platform/kms/overview.mdx:generic-api-key:281 docs/documentation/platform/kms/overview.mdx:generic-api-key:344 frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:85 +docs/cli/commands/user.mdx:generic-api-key:51 +frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76 +docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188 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 4b2ccae25..f060aa8ff 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -101,6 +101,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 { @@ -248,6 +249,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 8bc23bc5c..13f3bc306 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -432,6 +432,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, @@ -1018,6 +1028,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 c77a617b9..b71d51908 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 fc430c528..7fd77da6c 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -149,6 +149,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 ef37a953c..1f4badfb5 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -30,6 +30,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"; @@ -252,11 +253,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", @@ -329,6 +333,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" } @@ -2038,22 +2051,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; }; } @@ -2619,6 +2634,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 @@ -2781,8 +2856,8 @@ export type Event = | UpdateSlackIntegration | DeleteSlackIntegration | GetSlackIntegration - | UpdateProjectSlackConfig - | GetProjectSlackConfig + | UpdateProjectWorkflowIntegrationConfig + | GetProjectWorkflowIntegrationConfig | GetProjectSshConfig | UpdateProjectSshConfig | IntegrationSyncedEvent @@ -2859,4 +2934,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/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index dc90f6b3f..10454ab9b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1901,6 +1901,13 @@ export const AppConnections = { instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).", accessToken: "The access token to use to connect with Windmill." }, + HC_VAULT: { + instanceUrl: "The Hashicrop Vault instance URL to connect with.", + namespace: "The Hashicrop Vault namespace to connect with.", + accessToken: "The access token used to connect with Hashicorp Vault.", + roleId: "The Role ID used to connect with Hashicorp Vault.", + secretId: "The Secret ID used to connect with Hashicorp Vault." + }, LDAP: { provider: "The type of LDAP provider. Determines provider-specific behaviors.", url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').", @@ -2058,6 +2065,10 @@ export const SecretSyncs = { workspace: "The Windmill workspace to sync secrets to.", path: "The Windmill workspace path to sync secrets to." }, + HC_VAULT: { + mount: "The Hashicorp Vault Secrets Engine Mount to sync secrets to.", + path: "The Hashicorp Vault path to sync secrets to." + }, TEAMCITY: { project: "The TeamCity project to sync secrets to.", buildConfig: "The TeamCity build configuration to sync secrets to." 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 c4fceb90e..d15058bcb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -177,6 +177,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"; @@ -431,6 +434,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, @@ -628,6 +633,7 @@ export const registerRoutes = async ( tokenService, orgDAL, totpService, + orgMembershipDAL, auditLogService }); const passwordService = authPaswordServiceFactory({ @@ -692,6 +698,15 @@ export const registerRoutes = async ( orgDAL, externalGroupOrgRoleMappingDAL }); + + const microsoftTeamsService = microsoftTeamsServiceFactory({ + microsoftTeamsIntegrationDAL, + permissionService, + workflowIntegrationDAL, + kmsService, + serverCfgDAL: superAdminDAL + }); + const superAdminService = superAdminServiceFactory({ userDAL, identityDAL, @@ -705,7 +720,8 @@ export const registerRoutes = async ( orgService, keyStore, licenseService, - kmsService + kmsService, + microsoftTeamsService }); const orgAdminService = orgAdminServiceFactory({ @@ -1044,6 +1060,8 @@ export const registerRoutes = async ( certificateTemplateDAL, projectSlackConfigDAL, slackIntegrationDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsIntegrationDAL, projectTemplateService, groupProjectDAL, smtpService @@ -1168,7 +1186,9 @@ export const registerRoutes = async ( userDAL, licenseService, projectSlackConfigDAL, - resourceMetadataDAL + resourceMetadataDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }); const secretService = secretServiceFactory({ @@ -1230,7 +1250,9 @@ export const registerRoutes = async ( accessApprovalPolicyApproverDAL, projectSlackConfigDAL, kmsService, - groupDAL + groupDAL, + microsoftTeamsService, + projectMicrosoftTeamsConfigDAL }); const secretReplicationService = secretReplicationServiceFactory({ @@ -1628,6 +1650,7 @@ export const registerRoutes = async ( await dailyResourceCleanUp.startCleanUp(); await dailyExpiringPkiItemAlert.startSendingAlerts(); await kmsService.startService(); + await microsoftTeamsService.start(); // inject all services server.decorate("services", { @@ -1721,6 +1744,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/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index f6180b9b6..f6c260ea5 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -28,6 +28,10 @@ import { } from "@app/services/app-connection/databricks"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; +import { + HCVaultConnectionListItemSchema, + SanitizedHCVaultConnectionSchema +} from "@app/services/app-connection/hc-vault"; import { HumanitecConnectionListItemSchema, SanitizedHumanitecConnectionSchema @@ -68,6 +72,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedMsSqlConnectionSchema.options, ...SanitizedCamundaConnectionSchema.options, ...SanitizedAuth0ConnectionSchema.options, + ...SanitizedHCVaultConnectionSchema.options, ...SanitizedAzureClientSecretsConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options, ...SanitizedLdapConnectionSchema.options, @@ -88,6 +93,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ MsSqlConnectionListItemSchema, CamundaConnectionListItemSchema, Auth0ConnectionListItemSchema, + HCVaultConnectionListItemSchema, AzureClientSecretsConnectionListItemSchema, WindmillConnectionListItemSchema, LdapConnectionListItemSchema, diff --git a/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts new file mode 100644 index 000000000..061c02777 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts @@ -0,0 +1,47 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateHCVaultConnectionSchema, + SanitizedHCVaultConnectionSchema, + UpdateHCVaultConnectionSchema +} from "@app/services/app-connection/hc-vault"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerHCVaultConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.HCVault, + server, + sanitizedResponseSchema: SanitizedHCVaultConnectionSchema, + createSchema: CreateHCVaultConnectionSchema, + updateSchema: UpdateHCVaultConnectionSchema + }); + + // The following endpoints are for internal Infisical App use only and not part of the public API + server.route({ + method: "GET", + url: `/:connectionId/mounts`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.string().array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const mounts = await server.services.appConnection.hcvault.listMounts(connectionId, req.permission); + return mounts; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 194f32290..eeae5e5e3 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -9,6 +9,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; +import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; @@ -37,6 +38,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { 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/secret-sync-routers/hc-vault-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/hc-vault-sync-router.ts new file mode 100644 index 000000000..97ee6963b --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/hc-vault-sync-router.ts @@ -0,0 +1,17 @@ +import { + CreateHCVaultSyncSchema, + HCVaultSyncSchema, + UpdateHCVaultSyncSchema +} from "@app/services/secret-sync/hc-vault"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerHCVaultSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.HCVault, + server, + responseSchema: HCVaultSyncSchema, + createSchema: CreateHCVaultSyncSchema, + updateSchema: UpdateHCVaultSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index a54777727..75b3ac68e 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -8,6 +8,7 @@ import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; +import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; @@ -29,5 +30,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { const appCfg = getConfig(); @@ -342,8 +343,12 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { }` ); } + + const serverCfg = await getServerCfg(); return res.redirect( - `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` + `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${ + serverCfg.defaultAuthOrgId && !appCfg.isCloud ? `&defaultOrgAllowed=true` : "" + }` ); } }); 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/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index d9196dc88..552253cde 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -88,24 +88,41 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { rateLimit: authRateLimit }, schema: { - body: z.object({ - email: z.string().trim(), - firstName: z.string().trim(), - lastName: z.string().trim().optional(), - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - publicKey: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), - organizationName: GenericResourceNameSchema, - providerAuthToken: z.string().trim().optional().nullish(), - attributionSource: z.string().trim().optional(), - password: z.string() - }), + body: z + .object({ + email: z.string().trim(), + firstName: z.string().trim(), + lastName: z.string().trim().optional(), + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + publicKey: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim(), + providerAuthToken: z.string().trim().optional().nullish(), + attributionSource: z.string().trim().optional(), + password: z.string() + }) + .and( + z.preprocess( + (data) => { + if (typeof data === "object" && data && "useDefaultOrg" in data === false) { + return { ...data, useDefaultOrg: false }; + } + return data; + }, + z.discriminatedUnion("useDefaultOrg", [ + z.object({ useDefaultOrg: z.literal(true) }), + z.object({ + useDefaultOrg: z.literal(false), + organizationName: GenericResourceNameSchema + }) + ]) + ) + ), response: { 200: z.object({ message: z.string(), diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 82723a138..c2912c2b6 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -14,6 +14,7 @@ export enum AppConnection { Camunda = "camunda", Windmill = "windmill", Auth0 = "auth0", + HCVault = "hashicorp-vault", LDAP = "ldap", TeamCity = "teamcity" } diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 3b020fb06..95afdcbd2 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -41,6 +41,11 @@ import { } from "./databricks"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; +import { + getHCVaultConnectionListItem, + HCVaultConnectionMethod, + validateHCVaultConnectionCredentials +} from "./hc-vault"; import { getHumanitecConnectionListItem, HumanitecConnectionMethod, @@ -84,6 +89,7 @@ export const listAppConnectionOptions = () => { getAzureClientSecretsConnectionListItem(), getWindmillConnectionListItem(), getAuth0ConnectionListItem(), + getHCVaultConnectionListItem(), getLdapConnectionListItem(), getTeamCityConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); @@ -152,6 +158,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -186,10 +193,13 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case MsSqlConnectionMethod.UsernameAndPassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: + case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; + case HCVaultConnectionMethod.AppRole: + return "App Role"; case LdapConnectionMethod.SimpleBind: return "Simple Bind"; default: @@ -238,6 +248,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported, [AppConnection.Windmill]: platformManagedCredentialsNotSupported, [AppConnection.Auth0]: platformManagedCredentialsNotSupported, + [AppConnection.HCVault]: platformManagedCredentialsNotSupported, [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future [AppConnection.TeamCity]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 594c4c734..05e00446c 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -16,6 +16,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Camunda]: "Camunda", [AppConnection.Windmill]: "Windmill", [AppConnection.Auth0]: "Auth0", + [AppConnection.HCVault]: "Hashicorp Vault", [AppConnection.LDAP]: "LDAP", [AppConnection.TeamCity]: "TeamCity" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 9669de0c8..7a8b1a09c 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -43,6 +43,8 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { githubConnectionService } from "./github/github-connection-service"; +import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault"; +import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service"; import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; @@ -81,6 +83,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { + const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + + await blockLocalAndPrivateIpAddresses(instanceUrl); + + return instanceUrl; +}; + +export const getHCVaultConnectionListItem = () => ({ + name: "HCVault" as const, + app: AppConnection.HCVault as const, + methods: Object.values(HCVaultConnectionMethod) as [ + HCVaultConnectionMethod.AccessToken, + HCVaultConnectionMethod.AppRole + ] +}); + +type TokenRespData = { + auth: { + client_token: string; + }; +}; + +export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnectionCredentials) => { + // Return access token directly if not using AppRole method + if (connection.method !== HCVaultConnectionMethod.AppRole) { + return connection.credentials.accessToken; + } + + // Generate temporary token for AppRole method + try { + const { instanceUrl, roleId, secretId } = connection.credentials; + const tokenResp = await request.post( + `${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`, + { role_id: roleId, secret_id: secretId }, + { + headers: { + "Content-Type": "application/json", + ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) + } + } + ); + + if (tokenResp.status !== 200) { + throw new BadRequestError({ + message: `Unable to validate credentials: Hashicorp Vault responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.` + }); + } + + return tokenResp.data.auth.client_token; + } catch (e: unknown) { + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } +}; + +export const validateHCVaultConnectionCredentials = async (config: THCVaultConnectionConfig) => { + const instanceUrl = await getHCVaultInstanceUrl(config); + + try { + const accessToken = await getHCVaultAccessToken(config); + + // Verify token + await request.get(`${instanceUrl}/v1/auth/token/lookup-self`, { + headers: { "X-Vault-Token": accessToken } + }); + + return config.credentials; + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } +}; + +export const listHCVaultMounts = async (appConnection: THCVaultConnection) => { + const instanceUrl = await getHCVaultInstanceUrl(appConnection); + const accessToken = await getHCVaultAccessToken(appConnection); + + const { data } = await request.get(`${instanceUrl}/v1/sys/mounts`, { + headers: { + "X-Vault-Token": accessToken, + ...(appConnection.credentials.namespace ? { "X-Vault-Namespace": appConnection.credentials.namespace } : {}) + } + }); + + const mounts: string[] = []; + + // Filter for "kv" version 2 type only + Object.entries(data.data).forEach(([path, mount]) => { + if (mount.type === "kv" && mount.options?.version === "2") { + mounts.push(path); + } + }); + + return mounts; +}; diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts new file mode 100644 index 000000000..a6db4d0eb --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts @@ -0,0 +1,100 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { HCVaultConnectionMethod } from "./hc-vault-connection-enums"; + +const InstanceUrlSchema = z + .string() + .trim() + .min(1, "Instance URL required") + .url("Invalid Instance URL") + .describe(AppConnections.CREDENTIALS.HC_VAULT.instanceUrl); + +const NamespaceSchema = z.string().trim().optional().describe(AppConnections.CREDENTIALS.HC_VAULT.namespace); + +export const HCVaultConnectionAccessTokenCredentialsSchema = z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .describe(AppConnections.CREDENTIALS.HC_VAULT.accessToken) +}); + +export const HCVaultConnectionAppRoleCredentialsSchema = z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + roleId: z.string().trim().min(1, "Role ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.roleId), + secretId: z.string().trim().min(1, "Secret ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.secretId) +}); + +const BaseHCVaultConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.HCVault) }); + +export const HCVaultConnectionSchema = z.intersection( + BaseHCVaultConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(HCVaultConnectionMethod.AccessToken), + credentials: HCVaultConnectionAccessTokenCredentialsSchema + }), + z.object({ + method: z.literal(HCVaultConnectionMethod.AppRole), + credentials: HCVaultConnectionAppRoleCredentialsSchema + }) + ]) +); + +export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [ + BaseHCVaultConnectionSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AccessToken), + credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({}) + }), + BaseHCVaultConnectionSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AppRole), + credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({}) + }) +]); + +export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(HCVaultConnectionMethod.AccessToken) + .describe(AppConnections.CREATE(AppConnection.HCVault).method), + credentials: HCVaultConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.HCVault).credentials + ) + }), + z.object({ + method: z.literal(HCVaultConnectionMethod.AppRole).describe(AppConnections.CREATE(AppConnection.HCVault).method), + credentials: HCVaultConnectionAppRoleCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.HCVault).credentials + ) + }) +]); + +export const CreateHCVaultConnectionSchema = ValidateHCVaultConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault) +); + +export const UpdateHCVaultConnectionSchema = z + .object({ + credentials: z + .union([HCVaultConnectionAccessTokenCredentialsSchema, HCVaultConnectionAppRoleCredentialsSchema]) + .optional() + .describe(AppConnections.UPDATE(AppConnection.HCVault).credentials) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault)); + +export const HCVaultConnectionListItemSchema = z.object({ + name: z.literal("HCVault"), + app: z.literal(AppConnection.HCVault), + methods: z.nativeEnum(HCVaultConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts new file mode 100644 index 000000000..b5cee6fdd --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts @@ -0,0 +1,30 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listHCVaultMounts } from "./hc-vault-connection-fns"; +import { THCVaultConnection } from "./hc-vault-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const hcVaultConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listMounts = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor); + + try { + const mounts = await listHCVaultMounts(appConnection); + return mounts; + } catch (error) { + logger.error(error, "Failed to establish connection with Hashicorp Vault"); + return []; + } + }; + + return { + listMounts + }; +}; diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts new file mode 100644 index 000000000..6f254eda0 --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts @@ -0,0 +1,35 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateHCVaultConnectionSchema, + HCVaultConnectionSchema, + ValidateHCVaultConnectionCredentialsSchema +} from "./hc-vault-connection-schemas"; + +export type THCVaultConnection = z.infer; + +export type THCVaultConnectionInput = z.infer & { + app: AppConnection.HCVault; +}; + +export type TValidateHCVaultConnectionCredentialsSchema = typeof ValidateHCVaultConnectionCredentialsSchema; + +export type TValidateHCVaultConnectionCredentials = z.infer; + +export type THCVaultConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type THCVaultMountResponse = { + data: { + [key: string]: { + options: { + version?: string | null; + } | null; + type: string; // We're only interested in "kv" types + }; + }; +}; diff --git a/backend/src/services/app-connection/hc-vault/index.ts b/backend/src/services/app-connection/hc-vault/index.ts new file mode 100644 index 000000000..161c2b51c --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./hc-vault-connection-enums"; +export * from "./hc-vault-connection-fns"; +export * from "./hc-vault-connection-schemas"; +export * from "./hc-vault-connection-types"; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index d1b0a550d..14aa8f038 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -2,7 +2,7 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { Knex } from "knex"; -import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; @@ -20,6 +20,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TOrgDALFactory } from "../org/org-dal"; +import { getDefaultOrgMembershipRole } from "../org/org-role-fns"; +import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { LoginMethod } from "../super-admin/super-admin-types"; import { TTotpServiceFactory } from "../totp/totp-service"; @@ -48,6 +50,7 @@ type TAuthLoginServiceFactoryDep = { smtpService: TSmtpService; totpService: Pick; auditLogService: Pick; + orgMembershipDAL: TOrgMembershipDALFactory; }; export type TAuthLoginFactory = ReturnType; @@ -56,6 +59,7 @@ export const authLoginServiceFactory = ({ tokenService, smtpService, orgDAL, + orgMembershipDAL, totpService, auditLogService }: TAuthLoginServiceFactoryDep) => { @@ -719,6 +723,35 @@ export const authLoginServiceFactory = ({ authMethods: [authMethod], isGhost: false }); + + if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) { + let orgId = ""; + const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId); + if (!defaultOrg) { + throw new BadRequestError({ + message: `Failed to find default organization with ID ${serverCfg.defaultAuthOrgId}` + }); + } + orgId = defaultOrg.id; + const [orgMembership] = await orgDAL.findMembership({ + [`${TableName.OrgMembership}.userId` as "userId"]: user.id, + [`${TableName.OrgMembership}.orgId` as "id"]: orgId + }); + + if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole); + + await orgMembershipDAL.create({ + userId: user.id, + inviteEmail: email, + orgId, + role, + roleId, + status: OrgMembershipStatus.Accepted, + isActive: true + }); + } + } } else { const isLinkingRequired = !user?.authMethods?.includes(authMethod); if (isLinkingRequired) { diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 58ba9186e..4d8c98205 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; -import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { getMinExpiresIn } from "@app/lib/fn"; import { isDisposableEmail } from "@app/lib/validator"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; @@ -150,7 +150,8 @@ export const authSignupServiceFactory = ({ encryptedPrivateKeyTag, ip, userAgent, - authorization + authorization, + useDefaultOrg }: TCompleteAccountSignupDTO) => { const appCfg = getConfig(); const serverCfg = await getServerCfg(); @@ -293,15 +294,24 @@ export const authSignupServiceFactory = ({ }); if (!organizationId) { - const newOrganization = await orgService.createOrganization({ - userId: user.id, - userEmail: user.email ?? user.username, - orgName: organizationName - }); + let orgId = ""; + if (useDefaultOrg && serverCfg.defaultAuthOrgId && !appCfg.isCloud) { + const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId); + if (!defaultOrg) throw new BadRequestError({ message: "Failed to find default organization" }); + orgId = defaultOrg.id; + } else { + if (!organizationName) throw new BadRequestError({ message: "Organization name is required" }); + const newOrganization = await orgService.createOrganization({ + userId: user.id, + userEmail: user.email ?? user.username, + orgName: organizationName + }); - if (!newOrganization) throw new Error("Failed to create organization"); + if (!newOrganization) throw new Error("Failed to create organization"); + orgId = newOrganization.id; + } - organizationId = newOrganization.id; + organizationId = orgId; } const updatedMembersips = await orgDAL.updateMembership( diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts index 3308b9d12..8bbf302c5 100644 --- a/backend/src/services/auth/auth-signup-type.ts +++ b/backend/src/services/auth/auth-signup-type.ts @@ -12,12 +12,13 @@ export type TCompleteAccountSignupDTO = { encryptedPrivateKeyTag: string; salt: string; verifier: string; - organizationName: string; + organizationName?: string; providerAuthToken?: string | null; attributionSource?: string | undefined; ip: string; userAgent: string; authorization: string; + useDefaultOrg?: boolean; }; export type TCompleteAccountInviteDTO = { 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 0249d005f..8e60252ba 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -44,6 +44,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"; @@ -61,9 +64,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"; @@ -71,10 +76,11 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { TCreateProjectDTO, TDeleteProjectDTO, + TDeleteProjectWorkflowIntegration, TGetProjectDTO, TGetProjectKmsKey, - TGetProjectSlackConfig, TGetProjectSshConfig, + TGetProjectWorkflowIntegrationConfig, TListProjectAlertsDTO, TListProjectCasDTO, TListProjectCertificateTemplatesDTO, @@ -93,9 +99,9 @@ import { TUpdateProjectDTO, TUpdateProjectKmsDTO, TUpdateProjectNameDTO, - TUpdateProjectSlackConfig, TUpdateProjectSshConfig, TUpdateProjectVersionLimitDTO, + TUpdateProjectWorkflowIntegration, TUpgradeProjectDTO } from "./project-types"; @@ -124,8 +130,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; @@ -199,7 +216,9 @@ export const projectServiceFactory = ({ kmsService, projectBotDAL, projectSlackConfigDAL, + projectMicrosoftTeamsConfigDAL, slackIntegrationDAL, + microsoftTeamsIntegrationDAL, projectTemplateService, groupProjectDAL, smtpService @@ -1480,13 +1499,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({ @@ -1505,23 +1525,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({ @@ -1529,17 +1586,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` }); } @@ -1552,47 +1798,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` }); }; @@ -1702,10 +1929,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/secret-sync/hc-vault/hc-vault-sync-constants.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-constants.ts new file mode 100644 index 000000000..4210307d2 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const HC_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Hashicorp Vault", + destination: SecretSync.HCVault, + connection: AppConnection.HCVault, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts new file mode 100644 index 000000000..db35df292 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts @@ -0,0 +1,161 @@ +import { isAxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { getHCVaultAccessToken, getHCVaultInstanceUrl } from "@app/services/app-connection/hc-vault"; +import { + THCVaultListVariables, + THCVaultListVariablesResponse, + THCVaultSyncWithCredentials, + TPostHCVaultVariable +} from "@app/services/secret-sync/hc-vault/hc-vault-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => { + await blockLocalAndPrivateIpAddresses(instanceUrl); + + try { + const { data } = await request.get( + `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + { + headers: { + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}) + } + } + ); + + return data.data.data; + } catch (error: unknown) { + // Returning an empty set when a path isn't found allows that path to be created by a later POST request + if (isAxiosError(error) && error.response?.status === 404) { + return {}; + } + throw error; + } +}; + +// Hashicorp Vault updates all variables in one batch. This is to respect their versioning +const updateHCVaultVariables = async ({ + path, + instanceUrl, + namespace, + accessToken, + mount, + data +}: TPostHCVaultVariable) => { + await blockLocalAndPrivateIpAddresses(instanceUrl); + + return request.post( + `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + { + data + }, + { + headers: { + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}), + "Content-Type": "application/json" + } + } + ); +}; + +export const HCVaultSyncFns = { + syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { mount, path }, + syncOptions: { disableSecretDeletion } + } = secretSync; + + const { namespace } = connection.credentials; + const accessToken = await getHCVaultAccessToken(connection); + const instanceUrl = await getHCVaultInstanceUrl(connection); + + const variables = await listHCVaultVariables({ + instanceUrl, + accessToken, + namespace, + mount, + path + }); + let tainted = false; + + for (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + if (value !== variables[key]) { + variables[key] = value; + tainted = true; + } + } + + if (disableSecretDeletion) return; + + for await (const [key] of Object.entries(variables)) { + if (!(key in secretMap)) { + delete variables[key]; + tainted = true; + } + } + + // Only update variables if there was a change detected + if (!tainted) return; + + try { + await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + removeSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { mount, path } + } = secretSync; + + const { namespace } = connection.credentials; + const accessToken = await getHCVaultAccessToken(connection); + const instanceUrl = await getHCVaultInstanceUrl(connection); + + const variables = await listHCVaultVariables({ instanceUrl, namespace, accessToken, mount, path }); + + for await (const [key] of Object.entries(variables)) { + if (key in secretMap) { + delete variables[key]; + } + } + + try { + await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + getSecrets: async (secretSync: THCVaultSyncWithCredentials) => { + const { + connection, + destinationConfig: { mount, path } + } = secretSync; + + const { namespace } = connection.credentials; + const accessToken = await getHCVaultAccessToken(connection); + const instanceUrl = await getHCVaultInstanceUrl(connection); + + const variables = await listHCVaultVariables({ + instanceUrl, + namespace, + accessToken, + mount, + path + }); + + return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }])); + } +}; diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts new file mode 100644 index 000000000..d0f2a9f65 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts @@ -0,0 +1,58 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const HCVaultSyncDestinationConfigSchema = z.object({ + mount: z + .string() + .trim() + .min(1, "Secrets Engine Mount required") + .describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.mount), + path: z + .string() + .trim() + .min(1, "Path required") + .transform((val) => val.replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes + .refine((val) => new RE2("^([a-zA-Z0-9._-]+/)*[a-zA-Z0-9._-]+$").test(val), { + message: + "Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments." + }) + .describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.path) +}); + +const HCVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const HCVaultSyncSchema = BaseSecretSyncSchema(SecretSync.HCVault, HCVaultSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.HCVault), + destinationConfig: HCVaultSyncDestinationConfigSchema +}); + +export const CreateHCVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.HCVault, + HCVaultSyncOptionsConfig +).extend({ + destinationConfig: HCVaultSyncDestinationConfigSchema +}); + +export const UpdateHCVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.HCVault, + HCVaultSyncOptionsConfig +).extend({ + destinationConfig: HCVaultSyncDestinationConfigSchema.optional() +}); + +export const HCVaultSyncListItemSchema = z.object({ + name: z.literal("Hashicorp Vault"), + connection: z.literal(AppConnection.HCVault), + destination: z.literal(SecretSync.HCVault), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts new file mode 100644 index 000000000..4da823a76 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +import { THCVaultConnection } from "@app/services/app-connection/hc-vault"; + +import { CreateHCVaultSyncSchema, HCVaultSyncListItemSchema, HCVaultSyncSchema } from "./hc-vault-sync-schemas"; + +export type THCVaultSync = z.infer; + +export type THCVaultSyncInput = z.infer; + +export type THCVaultSyncListItem = z.infer; + +export type THCVaultSyncWithCredentials = THCVaultSync & { + connection: THCVaultConnection; +}; + +export type THCVaultListVariablesResponse = { + data: { + data: { + [key: string]: string; + }; + }; +}; + +export type THCVaultListVariables = { + accessToken: string; + instanceUrl: string; + namespace?: string; + mount: string; + path: string; +}; + +export type TPostHCVaultVariable = THCVaultListVariables & { + data: { + [key: string]: string; + }; +}; + +export type TDeleteHCVaultVariable = THCVaultListVariables; diff --git a/backend/src/services/secret-sync/hc-vault/index.ts b/backend/src/services/secret-sync/hc-vault/index.ts new file mode 100644 index 000000000..6fd9f4310 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./hc-vault-sync-constants"; +export * from "./hc-vault-sync-fns"; +export * from "./hc-vault-sync-schemas"; +export * from "./hc-vault-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 687d76f33..9d59ebb76 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -11,6 +11,7 @@ export enum SecretSync { Camunda = "camunda", Vercel = "vercel", Windmill = "windmill", + HCVault = "hashicorp-vault", TeamCity = "teamcity" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 143fa4622..f5737edb3 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -25,6 +25,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; +import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; @@ -45,6 +46,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, + [SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION, [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION }; @@ -142,6 +144,8 @@ export const SecretSyncFns = { return VercelSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.HCVault: + return HCVaultSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.TeamCity: return TeamCitySyncFns.syncSecrets(secretSync, secretMap); default: @@ -203,6 +207,9 @@ export const SecretSyncFns = { case SecretSync.Windmill: secretMap = await WindmillSyncFns.getSecrets(secretSync); break; + case SecretSync.HCVault: + secretMap = await HCVaultSyncFns.getSecrets(secretSync); + break; case SecretSync.TeamCity: secretMap = await TeamCitySyncFns.getSecrets(secretSync); break; @@ -259,6 +266,8 @@ export const SecretSyncFns = { return VercelSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.HCVault: + return HCVaultSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.TeamCity: return TeamCitySyncFns.removeSecrets(secretSync, secretMap); default: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index fdf4dfabb..c6d7adc8c 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -14,6 +14,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Camunda]: "Camunda", [SecretSync.Vercel]: "Vercel", [SecretSync.Windmill]: "Windmill", + [SecretSync.HCVault]: "Hashicorp Vault", [SecretSync.TeamCity]: "TeamCity" }; @@ -30,5 +31,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.TeamCity]: AppConnection.TeamCity }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index e99b31c20..e88174cc6 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -55,6 +55,12 @@ import { TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; +import { + THCVaultSync, + THCVaultSyncInput, + THCVaultSyncListItem, + THCVaultSyncWithCredentials +} from "./hc-vault/hc-vault-sync-types"; import { THumanitecSync, THumanitecSyncInput, @@ -88,6 +94,7 @@ export type TSecretSync = | TCamundaSync | TVercelSync | TWindmillSync + | THCVaultSync | TTeamCitySync; export type TSecretSyncWithCredentials = @@ -103,6 +110,7 @@ export type TSecretSyncWithCredentials = | TCamundaSyncWithCredentials | TVercelSyncWithCredentials | TWindmillSyncWithCredentials + | THCVaultSyncWithCredentials | TTeamCitySyncWithCredentials; export type TSecretSyncInput = @@ -118,6 +126,7 @@ export type TSecretSyncInput = | TCamundaSyncInput | TVercelSyncInput | TWindmillSyncInput + | THCVaultSyncInput | TTeamCitySyncInput; export type TSecretSyncListItem = @@ -133,6 +142,7 @@ export type TSecretSyncListItem = | TCamundaSyncListItem | TVercelSyncListItem | TWindmillSyncListItem + | THCVaultSyncListItem | TTeamCitySyncListItem; export type TSyncOptionsConfig = { 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/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index b8fc6ed7b..b14fd04e2 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -338,7 +338,7 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag parsedArguments.SetDefaults() - res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, parsedArguments.IsRecursive, "", *parsedArguments.ShouldExpandSecretReferences) + res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, true, parsedArguments.IsRecursive, "", *parsedArguments.ShouldExpandSecretReferences) if err != nil { return nil, err } diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/available.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/available.mdx new file mode 100644 index 000000000..ff52a325a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/hashicorp-vault/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/create.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/create.mdx new file mode 100644 index 000000000..6a978e780 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/hashicorp-vault" +--- + + + Check out the configuration docs for [Hashicorp Vault Connections](/integrations/app-connections/hashicorp-vault) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/delete.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/delete.mdx new file mode 100644 index 000000000..aaf378fb5 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/hashicorp-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-id.mdx new file mode 100644 index 000000000..39366f508 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/hashicorp-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-name.mdx new file mode 100644 index 000000000..9f3c41783 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/hashicorp-vault/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/list.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/list.mdx new file mode 100644 index 000000000..474c70fd8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/hashicorp-vault" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/update.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/update.mdx new file mode 100644 index 000000000..e155c8e67 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/hashicorp-vault/{connectionId}" +--- + + + Check out the configuration docs for [Hashicorp Vault Connections](/integrations/app-connections/hashicorp-vault) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/create.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/create.mdx new file mode 100644 index 000000000..ab9171f7b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/delete.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/delete.mdx new file mode 100644 index 000000000..700438ba5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/hashicorp-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id.mdx new file mode 100644 index 000000000..7017416c1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/hashicorp-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name.mdx new file mode 100644 index 000000000..a817732f1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/hashicorp-vault/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets.mdx new file mode 100644 index 000000000..3ee2c479c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/list.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/list.mdx new file mode 100644 index 000000000..e3c08f125 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/hashicorp-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets.mdx new file mode 100644 index 000000000..7b54e94d6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets.mdx new file mode 100644 index 000000000..24f58d802 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/update.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/update.mdx new file mode 100644 index 000000000..58ddc5a2b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/hashicorp-vault/{syncId}" +--- diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index bde87f42f..e8b532c1c 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -45,6 +45,10 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) + + Currently, the following JWT signature algorithms are supported: RS256, RS512, HS256, and EdDSA + + Once you've done that, press **Update** to complete the required configuration. diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx index 11216b893..76e364b2f 100644 --- a/docs/documentation/platform/sso/general-oidc.mdx +++ b/docs/documentation/platform/sso/general-oidc.mdx @@ -44,7 +44,9 @@ Prerequisites: To configure OIDC via the custom endpoints, set the **Configuration Type** field to **Custom** and input the required endpoint fields. ![OIDC general custom config](../../../images/sso/general-oidc/custom-oidc-form.png) - 2.3. Optionally, you can define a whitelist of allowed email domains. + 2.3. Select the appropriate JWT signature algorithm for your IdP. Currently, the supported options are RS256, RS512, HS256, and EdDSA. + + 2.4. Optionally, you can define a whitelist of allowed email domains. Finally, fill out the **Client ID** and **Client Secret** fields and press **Update** to complete the required configuration. diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx index 6d8f4e4c8..803818a0e 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -72,6 +72,10 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**. ![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png) + + Currently, the following JWT signature algorithms are supported: RS256, RS512, HS256, and EdDSA + + Once you've done that, press **Update** to complete the required configuration. diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 0d4b8da89..e5d5e5c16 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -35,6 +35,10 @@ Infisical supports these and many other identity providers: If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance. + + For enhanced security, Infisical enforces PKCE (Proof Key for Code Exchange) with the OAuth 2.0-based SSO providers and OIDC. This provides additional protection against authorization code interception attacks and strengthens your authentication flow security. + + ## FAQ 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/app-connections/hashicorp-vault/vault-access.png b/docs/images/app-connections/hashicorp-vault/vault-access.png new file mode 100644 index 000000000..b6b6504fe Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-access.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-approle.png b/docs/images/app-connections/hashicorp-vault/vault-approle.png new file mode 100644 index 000000000..89cff90fe Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-approle.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-authentication-methods.png b/docs/images/app-connections/hashicorp-vault/vault-authentication-methods.png new file mode 100644 index 000000000..fd9d607b9 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-authentication-methods.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png b/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png new file mode 100644 index 000000000..57af4514f Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-create-policy.png b/docs/images/app-connections/hashicorp-vault/vault-create-policy.png new file mode 100644 index 000000000..5d429fcc3 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-create-policy.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-enable-method.png b/docs/images/app-connections/hashicorp-vault/vault-enable-method.png new file mode 100644 index 000000000..c034995ac Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-enable-method.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png new file mode 100644 index 000000000..872d11cab Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png new file mode 100644 index 000000000..4a91f8dbb Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png new file mode 100644 index 000000000..90d562e11 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-policies-navigate.png b/docs/images/app-connections/hashicorp-vault/vault-policies-navigate.png new file mode 100644 index 000000000..05cab30eb Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-policies-navigate.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-policies-page.png b/docs/images/app-connections/hashicorp-vault/vault-policies-page.png new file mode 100644 index 000000000..3a84b86ee Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-policies-page.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-profile-token.png b/docs/images/app-connections/hashicorp-vault/vault-profile-token.png new file mode 100644 index 000000000..aca3b7f0a Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-profile-token.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-shell-output.png b/docs/images/app-connections/hashicorp-vault/vault-shell-output.png new file mode 100644 index 000000000..a5d654110 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-shell-output.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-shell.png b/docs/images/app-connections/hashicorp-vault/vault-shell.png new file mode 100644 index 000000000..ca3ff0bc4 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-shell.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-token.png b/docs/images/app-connections/hashicorp-vault/vault-token.png new file mode 100644 index 000000000..aca3b7f0a Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-token.png differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png deleted file mode 100644 index 367386709..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png deleted file mode 100644 index 80de8df26..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png deleted file mode 100644 index d51142541..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png deleted file mode 100644 index b587777f9..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png deleted file mode 100644 index 619764b54..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png deleted file mode 100644 index 7fdef0d4a..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png deleted file mode 100644 index a65870b44..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png deleted file mode 100644 index 34b03768f..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png deleted file mode 100644 index 624fdc574..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png deleted file mode 100644 index e2a77654a..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png deleted file mode 100644 index 719659014..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png deleted file mode 100644 index 76fe2de35..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png deleted file mode 100644 index 7d63bde40..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault.png deleted file mode 100644 index e556ffc6b..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault.png and /dev/null differ 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/images/secret-syncs/hashicorp-vault/select-option.png b/docs/images/secret-syncs/hashicorp-vault/select-option.png new file mode 100644 index 000000000..3ed6ce079 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/select-option.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-created.png b/docs/images/secret-syncs/hashicorp-vault/sync-created.png new file mode 100644 index 000000000..6fc827f2c Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-created.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-destination.png b/docs/images/secret-syncs/hashicorp-vault/sync-destination.png new file mode 100644 index 000000000..42ec57273 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-destination.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-details.png b/docs/images/secret-syncs/hashicorp-vault/sync-details.png new file mode 100644 index 000000000..61f045db0 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-details.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-options.png b/docs/images/secret-syncs/hashicorp-vault/sync-options.png new file mode 100644 index 000000000..fd96843f0 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-options.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-review.png b/docs/images/secret-syncs/hashicorp-vault/sync-review.png new file mode 100644 index 000000000..f95a12e0c Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-review.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-source.png b/docs/images/secret-syncs/hashicorp-vault/sync-source.png new file mode 100644 index 000000000..b3440a8df Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-source.png differ diff --git a/docs/images/sso/general-oidc/custom-oidc-form.png b/docs/images/sso/general-oidc/custom-oidc-form.png index 2aee02680..92a7a056d 100644 Binary files a/docs/images/sso/general-oidc/custom-oidc-form.png and b/docs/images/sso/general-oidc/custom-oidc-form.png differ diff --git a/docs/images/sso/general-oidc/discovery-oidc-form.png b/docs/images/sso/general-oidc/discovery-oidc-form.png index ae99b35b2..fb0daf1e7 100644 Binary files a/docs/images/sso/general-oidc/discovery-oidc-form.png and b/docs/images/sso/general-oidc/discovery-oidc-form.png differ diff --git a/docs/integrations/app-connections/hashicorp-vault.mdx b/docs/integrations/app-connections/hashicorp-vault.mdx new file mode 100644 index 000000000..6e2ff68bf --- /dev/null +++ b/docs/integrations/app-connections/hashicorp-vault.mdx @@ -0,0 +1,214 @@ +--- +title: "Hashicorp Vault Connection" +description: "Learn how to configure a Hashicorp Vault Connection for Infisical." +--- + + + Infisical is compatible with Vault Self-hosted, HCP Vault Dedicated, and HCP Vault Enterprise deployments. Please note that HCP Generic Secrets are currently not supported. + + +Infisical supports two methods for connecting to Hashicorp Vault. + + + + + + ![Vault Access](/images/app-connections/hashicorp-vault/vault-access.png) + + + In the **Authentication Methods** tab, click on **Enable new method**. + + ![Vault Enable Method](/images/app-connections/hashicorp-vault/vault-authentication-methods.png) + + + ![Vault AppRole](/images/app-connections/hashicorp-vault/vault-approle.png) + + + You may change the name of the method, but we suggest keeping it as `approle`. + + ![Vault Enable Method](/images/app-connections/hashicorp-vault/vault-enable-method.png) + + + From the home page, navigate to **Policies**. + + ![Vault Policies Navigate](/images/app-connections/hashicorp-vault/vault-policies-navigate.png) + + + ![Vault Policies Page](/images/app-connections/hashicorp-vault/vault-policies-page.png) + + + You may name your policy whatever you want, but remember the name as it will be used in future steps. + + Depending on your use case, you may have different policy configurations: + + + + ```hcl + path "demo_mount/data/*" { + capabilities = [ "create", "read", "update" ] + } + + path "sys/mounts" { + capabilities = ["read"] + } + ``` + + - **demo_mount**: The name of the target secrets engine (e.g., 'secret', 'kv'). + - **data/\***: The path within the secrets engine used for storing secrets. The wildcard (*) grants access to all secrets within this mount point. + + + Make sure to replace the policy path with the specific path where you intend to sync your secrets. For better security and control, it's recommended to use a more granular path instead of a wildcard (*). You can also specify a path that doesn’t yet exist—Infisical will automatically create it for you during the sync process. + + + + + ![Vault Create Policy](/images/app-connections/hashicorp-vault/vault-create-policy.png) + + + **Open Vault Shell** + + ![Vault Shell](/images/app-connections/hashicorp-vault/vault-shell.png) + + + If you used custom approle or policy names in previous steps, you'll need to customize the following commands. + + + **Create Infisical Role** + + ```hcl + vault write auth/approle/role/infisical token_policies="infisical-policy" token_ttl=30s token_max_ttl=2m + ``` + + **Read RoleID** + + ```hcl + vault read auth/approle/role/infisical/role-id + ``` + + **Generate New SecretID** + + ```hcl + vault write -force auth/approle/role/infisical/secret-id + ``` + + Your shell output should look similar to the image below. Save the RoleID and SecretID values for later steps. + + ![Vault Shell Output](/images/app-connections/hashicorp-vault/vault-shell-output.png) + + + + + ## Get a Hashicorp Vault Access Token + + Open your profile dropdown and click **Copy token**. This token will be used in later steps. + + ![Vault Profile Copy Token](/images/app-connections/hashicorp-vault/vault-profile-token.png) + + + +## Getting Vault Instance URL + + + + For self-hosted instances, locate and copy your vault's base URL (for example: `https://vault.example.com`). + + Save the URL for later steps. + + + On HCP instances, you may need to navigate to **Cluster Overview** to see your cluster URL. Save this value for later steps. + + ![Vault Cluster URLs](/images/app-connections/hashicorp-vault/vault-cluster-urls.png) + + + Cluster Overview is found in the HCP dashboard, not in your cluster's web UI. + + + + +## Setup Vault Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **Hashicorp Vault Connection** option. + + ![Select Vault Connection](/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png) + + + Configure your Vault Connection using the Instance URL and credentials from the steps above. **Depending on if you chose to authenticate with an Access Token or AppRole, you may need to input different information.** + + ![Vault Configure Connection](/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png) + + + + - **Name**: The name of the connection being created. Must be slug-friendly. + - **Description**: An optional description to provide details about this connection. + - **Instance URL**: The URL of your Hashicorp Vault instance. + - **Namespace (optional)**: The namespace within your vault. Self-hosted and enterprise clusters may not use namespaces. + - **Role ID**: The Role ID generated in the steps above. + - **Secret ID**: The Secret ID generated in the steps above. + + + - **Name**: The name of the connection being created. Must be slug-friendly. + - **Description**: An optional description to provide details about this connection. + - **Instance URL**: The URL of your Hashicorp Vault instance. + - **Namespace (optional)**: The namespace within your vault. Self-hosted and enterprise clusters may not use namespaces. + - **Access Token**: The Access Token generated in the steps above. + + + + + Your Vault Connection is now available for use. + ![Vault Connection Created](/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png) + + + + + To create a Vault Connection, make an API request to the [Create Hashicorp Vault + Connection](/api-reference/endpoints/app-connections/hashicorp-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/hashicorp-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-vault-connection", + "method": "app-role", + "credentials": { + "instanceUrl": "https://vault.example.com", + "roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf", + "secretId": "ad24df93-19c8-c865-9997-6b8513253d3a" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-vault-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2025-04-01T05:31:56Z", + "updatedAt": "2025-04-01T05:31:56Z", + "app": "hashicorp-vault", + "method": "app-role", + "credentials": { + "instanceUrl": "https://vault.example.com", + "roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf" + } + } + } + ``` + + diff --git a/docs/integrations/cloud/hashicorp-vault.mdx b/docs/integrations/cloud/hashicorp-vault.mdx index 51a66f3ff..df2542ce7 100644 --- a/docs/integrations/cloud/hashicorp-vault.mdx +++ b/docs/integrations/cloud/hashicorp-vault.mdx @@ -4,158 +4,5 @@ description: "How to sync secrets from Infisical to HashiCorp Vault" --- - Infisical connects to Vault via the AppRole auth method. - - Currently, each Infisical project can only point and sync secrets to one Vault cluster / namespace - but with unlimited integrations to different paths within it. - - This tutorial makes use of Vault's UI but, in principle, instructions can executed via - Vault CLI or API call. - - Lastly, you should note that we provide a simple use-case and, in practice, you should adapt and extend it to your own Vault use-case and follow best practices, for instance when defining fine-grained ACL policies. + The Hashicorp Vault Native Integration will be deprecated in 2026. Please migrate to our new [Hashicorp Vault Sync](../secret-syncs/hashicorp-vault). - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -- Have experience with [HashiCorp Vault](https://www.vaultproject.io/). - -## Navigate to your project's integrations tab - -![integrations](../../images/integrations.png) - -## Prepare Vault - -This section mirrors the latter parts of the [Vault quickstart](https://developer.hashicorp.com/vault/tutorials/cloud/getting-started-intro) provided by HashiCorp and uses sample names/values for demonstration. - -To begin, navigate to the cluster / namespace that you want to sync secrets to in Vault; we'll use the default `admin` namespace (in practice, we recommend creating a namespace and not using the default `admin` namespace). - -### Enable KV Secrets Engine - -In Secrets, enable a KV Secrets Engine at a path for Infisical to sync secrets to; we'll use the path `kv`. - -![integrations hashicorp vault secrets engine](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png) -![integrations hashicorp vault secrets engine](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png) -![integrations hashicorp vault secrets engine](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png) - -### Enable the AppRole auth method - -In Access > Auth Methods, enable the AppRole auth method. - -![integrations hashicorp vault access](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png) -![integrations hashicorp vault access](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png) -![integrations hashicorp vault access](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png) - -### Create an ACL Policy - -Now in Policies, create a new ACL policy scoped to the path(s) you wish Infisical to be able to sync secrets to. - -We'll call the policy `test` and have it grant access to the `dev` path in the KV Secrets Engine where we will be syncing secrets to from Infisical. - -```console -path "kv/data/dev" { - capabilities = [ "create", "read", "update" ] -} - -path "sys/namespaces/*" { - capabilities = [ "create", "read", "update", "delete", "list" ] -} -``` - - - `kv` comes from the path of the KV Secrets Engine that we enabled and `dev` is the chosen path within it - that we want to sync secrets to. - - -![integrations hashicorp vault policy](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png) -![integrations hashicorp vault policy](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png) -![integrations hashicorp vault policy](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png) - -### Create a role with the policy attached - -We now create a `infisical` role with the generated token's time-to-live (TTL) set to 1 hour and can be renewed for up to 4 hours from the time of its creation. - -1. Click the Vault CLI shell icon (`>_`) to open a command shell in the browser. - -![integrations hashicorp vault shell](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png) - -2. Copy the command below. - -```console -vault write auth/approle/role/infisical token_policies="test" token_ttl=1h token_max_ttl=4h -``` - -3. Paste the command into the command shell in the browser and press the enter button. - -### Generate a RoleID and SecretID - -Finally, we need to generate a **RoleID** and **SecretID** (like a username and password) that Infisical can use -to authenticate with Vault. - -1. Click the Vault CLI shell icon (>_) again to open a command shell. - -2. Read the RoleID. - -```console -vault read auth/approle/role/infisical/role-id -``` - -Example output: - -```console -Key Value -role_id b6ccdcca-183b-ce9c-6b98-b556b9a0edb9 -``` - -3. Generate a new SecretID of the `infisical` role. - -```console -vault write -force auth/approle/role/infisical/secret-id -``` - -Example output: - - -```console -Key Value -secret_id 735a47cc-7a98-77cc-0128-12b1e96a4157 -secret_id_accessor 3ab305d1-1eab-df4b-4079-ef7135635c49 -...snip... -``` - -Great. We're now ready to connect Infisical to Vault! - -## Enter your Vault instance and authentication details - -Back in Infisical, press on the HashiCorp Vault tile and input your Vault instance and `infisical` role RoleID and SecretID. - -![integrations hashicorp vault authorization](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png) - -For additional details on each field: - -- Vault Cluster URL: The address of your cluster, either HCP or self-hosted. - -If using HCP, you can copy your Cluster URL in the Cluster Overview: - -![integrations hashicorp vault cluster URL](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png) - -- Vault Namespace: The Vault namespace you wish to connect to. -- Vault RoleID: The RoleID previously created for the `infisical` role. -- Vault SecretID: The SecretID previously created for the `infisical` role. - -## Start integration - -Select which Infisical environment secrets you want to sync to Vault. - -For additional details on each field: - -- Vault KV Secrets Engine Path: the path at which you enabled the intended KV Secrets Engine; in this demonstration, we used `kv`. -- Vault Secret(s) Path: the path in the KV Secrets Engine that you wish to sync secrets to. - -Press create integration to start syncing secrets to Vault. - -![integrations hashicorp vault](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png) -![integrations hashicorp vault](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault.png) - - - diff --git a/docs/integrations/secret-syncs/hashicorp-vault.mdx b/docs/integrations/secret-syncs/hashicorp-vault.mdx new file mode 100644 index 000000000..0d6c0d644 --- /dev/null +++ b/docs/integrations/secret-syncs/hashicorp-vault.mdx @@ -0,0 +1,160 @@ +--- +title: "Hashicorp Vault Sync" +description: "Learn how to configure a Hashicorp Vault Sync for Infisical." +--- + +**Prerequisites:** + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Hashicorp Vault Connection](/integrations/app-connections/hashicorp-vault) + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select Hashicorp Vault](/images/secret-syncs/hashicorp-vault/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/hashicorp-vault/sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed. + + ![Configure Destination](/images/secret-syncs/hashicorp-vault/sync-destination.png) + + - **Hashicorp Vault Connection**: The Vault Connection to authenticate with. + - **Secrets Engine Mount**: The secrets engine to sync secrets with (e.g., 'secret', 'kv'). + - **Path**: The specific path within the secrets engine where secrets will be stored. + + After configuring these parameters, click the **Next** button to continue to the Sync Options step. + + + If the **path** you provide does not exist in Vault, it will be created. + + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/hashicorp-vault/sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Hashicorp Vault when keys conflict. + - **Import Secrets (Prioritize Hashicorp Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Hashicorp Vault over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your Hashicorp Vault Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/hashicorp-vault/sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Hashicorp Vault Sync configuration, then click **Create Sync**. + + ![Confirm Configuration](/images/secret-syncs/hashicorp-vault/sync-review.png) + + + If enabled, your Hashicorp Vault Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/hashicorp-vault/sync-created.png) + + + + + To create an **Hashicorp Vault Sync**, make an API request to the [Create Hashicorp Vault Sync](/api-reference/endpoints/secret-syncs/hashicorp-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/hashicorp-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-vault-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "mount": "secret", + "path": "dev/nested" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-vault-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "hashicorp-vault", + "name": "my-vault-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "destination": "hashicorp-vault", + "destinationConfig": { + "mount": "secret", + "path": "dev/nested" + } + } + } + ``` + + diff --git a/docs/internals/bug-bounty.mdx b/docs/internals/bug-bounty.mdx new file mode 100644 index 000000000..b823e6246 --- /dev/null +++ b/docs/internals/bug-bounty.mdx @@ -0,0 +1,60 @@ +--- +title: "Bug bounty program" +description: " Learn about our bug bounty program and how to report vulnerabilities." +--- + +The Infisical Bug Bounty Program is our way of recognizing and rewarding the work of security researchers who help keep our platform secure. By reporting vulnerabilities or potential risks, you help us protect secrets, infrastructure, and the organizations who rely on us. + +We value reports that help identify vulnerabilities that affect the integrity of secrets, prevent unauthorized access to environments, or expose flaws in our authentication or authorization flows. + +### How to Report + +- Send reports to **security@infisical.com** with clear steps to reproduce, impact, and (if possible) a proof-of-concept. +- We will acknowledge receipt within 3 business days. +- We'll provide an initial assessment or next steps within 5 business days. + +### What's in Scope? + +- Vulnerabilities in our cloud-hosted platform (e.g., `app.infisical.com`, `eu.infisical.com`) +- Security issues in the open source Infisical codebase, as maintained in our official GitHub repository +- Authentication bypass, privilege escalation, or access to secrets/data without authorization + +### Reward Guidelines + +Bounties are based on severity, impact, and exploitability, as well as whether the report introduces a new vulnerability class or helps improve an existing fix. + +| Severity | Examples | Typical Reward (USD currency) | +| --- | --- | --- | +| **Critical** | Full unauthorized access to secrets, authentication bypass, cross-tenant access, RCE, full compromise, etc | $2,000 - $5,000 | +| **High** | Privilege escalation, project-level access without authorization, persistent DoS | $750 - $2,000 | +| **Medium** | Info disclosure, scoped DoS (e.g. ReDoS with auth), or minor access control issues | $250 - $1,000 | +| **Low / Informational** | Missing headers, CSP warnings, theoretical flaws, self-hosting misconfigurations | Recognition only | + + +We may award lower amounts for: +- Duplicate class vulnerabilities already under review +- Patch bypasses of previously rewarded issues +- Vulnerabilities requiring unrealistic attacker conditions + +All final reward amounts are determined at Infisical's discretion based on impact, report quality, and how actionable the issue is. + + +### Out of Scope + +- Social engineering or phishing +- Rate limiting issues on non-sensitive endpoints +- Denial-of-service attacks that require authentication and don't impact core service availability +- Findings based on outdated or forked code not maintained by the Infisical team +- Vulnerabilities in third-party dependencies unless they result in a direct risk to Infisical users + + +### Responsible Disclosure + +We ask that researchers: + +- Avoid accessing data that isn't yours +- Do not publicly disclose without coordination +- Use testing accounts where possible +- Give us a reasonable window to investigate and patch before going public + +Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally. \ No newline at end of file diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx index 17daf88cb..219c32287 100644 --- a/docs/internals/security.mdx +++ b/docs/internals/security.mdx @@ -118,8 +118,6 @@ It should be noted that, even on Infisical Cloud, it is physically impossible fo Please email security@infisical.com if you have any specific inquiries about employee data and security policies. -## Get in touch - -If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible. - -Note that this security address should be used for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly. +## Bug Bounty Program +We run a [Bug Bounty Program](/internals/bug-bounty) to recognize and reward security researchers who help make Infisical more secure. +If you've found a vulnerability, please review the program details for scope, disclosure guidelines, and reward tiers. \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index f629baf46..8f5cd4d7d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -228,7 +228,8 @@ { "group": "Workflow Integrations", "pages": [ - "documentation/platform/workflow-integrations/slack-integration" + "documentation/platform/workflow-integrations/slack-integration", + "documentation/platform/workflow-integrations/microsoft-teams-integration" ] }, { @@ -443,6 +444,7 @@ "integrations/app-connections/databricks", "integrations/app-connections/gcp", "integrations/app-connections/github", + "integrations/app-connections/hashicorp-vault", "integrations/app-connections/humanitec", "integrations/app-connections/ldap", "integrations/app-connections/mssql", @@ -470,6 +472,7 @@ "integrations/secret-syncs/databricks", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", + "integrations/secret-syncs/hashicorp-vault", "integrations/secret-syncs/humanitec", "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", @@ -1073,6 +1076,18 @@ "api-reference/endpoints/app-connections/github/delete" ] }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/app-connections/hashicorp-vault/list", + "api-reference/endpoints/app-connections/hashicorp-vault/available", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-id", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-name", + "api-reference/endpoints/app-connections/hashicorp-vault/create", + "api-reference/endpoints/app-connections/hashicorp-vault/update", + "api-reference/endpoints/app-connections/hashicorp-vault/delete" + ] + }, { "group": "Humanitec", "pages": [ @@ -1285,6 +1300,20 @@ "api-reference/endpoints/secret-syncs/github/remove-secrets" ] }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/hashicorp-vault/list", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name", + "api-reference/endpoints/secret-syncs/hashicorp-vault/create", + "api-reference/endpoints/secret-syncs/hashicorp-vault/update", + "api-reference/endpoints/secret-syncs/hashicorp-vault/delete", + "api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets" + ] + }, { "group": "Humanitec", "pages": [ @@ -1554,6 +1583,7 @@ }, "internals/components", "internals/security", + "internals/bug-bounty", "internals/service-tokens" ] }, 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/auth/Mfa.tsx b/frontend/src/components/auth/Mfa.tsx index 3eff8a89c..64c8a5ee6 100644 --- a/frontend/src/components/auth/Mfa.tsx +++ b/frontend/src/components/auth/Mfa.tsx @@ -31,6 +31,25 @@ const codeInputProps = { } } as const; +const codeInputPropsPhone = { + inputStyle: { + fontFamily: "monospace", + margin: "4px", + MozAppearance: "textfield", + width: "40px", + borderRadius: "5px", + fontSize: "24px", + height: "40px", + paddingLeft: "7", + backgroundColor: "#0d1117", + color: "white", + border: "1px solid #2d2f33", + textAlign: "center", + outlineColor: "#8ca542", + borderColor: "#2d2f33" + } +} as const; + type Props = { successCallback: () => void | Promise; closeMfa?: () => void; @@ -172,6 +191,24 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop )} +
+ {method === MfaMethod.EMAIL && ( + + )} + {method === MfaMethod.TOTP && ( +
+ setMfaCode(e.target.value)} /> +
+ )} +
{typeof triesLeft === "number" && ( )} diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx new file mode 100644 index 000000000..76e14b6d7 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx @@ -0,0 +1,86 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2"; +import { useHCVaultConnectionListMounts } from "@app/hooks/api/appConnections/hc-vault"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const HCVaultSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.HCVault } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: mounts, isLoading: isMountsLoading } = useHCVaultConnectionListMounts( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + return ( + <> + { + setValue("destinationConfig.mount", ""); + setValue("destinationConfig.path", ""); + }} + /> + + ( + +
+ Don't see the mount you're looking for?{" "} + +
+ + } + > + + onChange((option as SingleValue<{ value: string }>)?.value ?? null) + } + options={mounts?.map((v) => ({ label: v, value: v }))} + placeholder="Select a Secrets Engine Mount..." + /> +
+ )} + /> + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 47afc6f04..1d7a1dd55 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -11,6 +11,7 @@ import { CamundaSyncFields } from "./CamundaSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; +import { HCVaultSyncFields } from "./HCVaultSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; @@ -47,6 +48,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Windmill: return ; + case SecretSync.HCVault: + return ; case SecretSync.TeamCity: return ; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index e2c285b6f..e4aa4ad65 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Camunda: case SecretSync.Vercel: case SecretSync.Windmill: + case SecretSync.HCVault: case SecretSync.TeamCity: AdditionalSyncOptionsFieldsComponent = null; break; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx new file mode 100644 index 000000000..2e1abac21 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx @@ -0,0 +1,18 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const HCVaultSyncReviewFields = () => { + const { watch } = useFormContext(); + const mount = watch("destinationConfig.mount"); + const path = watch("destinationConfig.path"); + + return ( + <> + {mount} + {path} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index da9651535..62402e540 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -21,6 +21,7 @@ import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; +import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; @@ -89,6 +90,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Windmill: DestinationFieldsComponent = ; break; + case SecretSync.HCVault: + DestinationFieldsComponent = ; + break; case SecretSync.TeamCity: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts new file mode 100644 index 000000000..a02778aaf --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const HCVaultSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.HCVault), + destinationConfig: z.object({ + mount: z.string().trim().min(1, "Secrets Engine Mount required"), + path: z + .string() + .trim() + .min(1, "Path required") + .transform((val) => val.trim().replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes + .refine((val) => /^([a-zA-Z0-9._-]+\/)*[a-zA-Z0-9._-]+$/.test(val), { + message: + "Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments." + }) + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 50221dc39..bc6184bc7 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -8,6 +8,7 @@ import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema" import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; +import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; @@ -27,6 +28,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ CamundaSyncDestinationSchema, VercelSyncDestinationSchema, WindmillSyncDestinationSchema, + HCVaultSyncDestinationSchema, TeamCitySyncDestinationSchema ]); 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 aea43a52b..5efe7ca69 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/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 35d481b82..68715f9a0 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -19,6 +19,7 @@ import { DatabricksConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, + HCVaultConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, MsSqlConnectionMethod, @@ -58,6 +59,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }, [AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" }, [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 }, + [AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 }, [AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 }, [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" } }; @@ -88,11 +90,14 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: return { name: "Username & Password", icon: faLock }; + case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: return { name: "Client Credentials", icon: faServer }; + case HCVaultConnectionMethod.AppRole: + return { name: "App Role", icon: faUser }; case LdapConnectionMethod.SimpleBind: return { name: "Simple Bind", icon: faLink }; default: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 009c2804b..58d9f3e48 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -40,6 +40,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.TeamCity]: AppConnection.TeamCity }; 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/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index daa0eb1c3..5e1f84cb4 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -14,6 +14,7 @@ export enum AppConnection { Camunda = "camunda", Windmill = "windmill", Auth0 = "auth0", + HCVault = "hashicorp-vault", LDAP = "ldap", TeamCity = "teamcity" } diff --git a/frontend/src/hooks/api/appConnections/hc-vault/index.ts b/frontend/src/hooks/api/appConnections/hc-vault/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/hc-vault/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx new file mode 100644 index 000000000..6f1b6ae46 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; + +const hcVaultConnectionKeys = { + all: [...appConnectionKeys.all, "hcvault"] as const, + listMounts: (connectionId: string) => + [...hcVaultConnectionKeys.all, "mounts", connectionId] as const +}; + +export const useHCVaultConnectionListMounts = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + string[], + unknown, + string[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: hcVaultConnectionKeys.listMounts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/hashicorp-vault/${connectionId}/mounts` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 9e9100d57..910716c02 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -72,6 +72,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Auth0; }; +export type THCVaultConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.HCVault; +}; + export type TLdapConnectionOption = TAppConnectionOptionBase & { app: AppConnection.LDAP; }; @@ -96,6 +100,7 @@ export type TAppConnectionOption = | TCamundaConnectionOption | TWindmillConnectionOption | TAuth0ConnectionOption + | THCVaultConnectionOption | TTeamCityConnectionOption; export type TAppConnectionOptionMap = { @@ -114,6 +119,7 @@ export type TAppConnectionOptionMap = { [AppConnection.Camunda]: TCamundaConnectionOption; [AppConnection.Windmill]: TWindmillConnectionOption; [AppConnection.Auth0]: TAuth0ConnectionOption; + [AppConnection.HCVault]: THCVaultConnectionOption; [AppConnection.LDAP]: TLdapConnectionOption; [AppConnection.TeamCity]: TTeamCityConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts new file mode 100644 index 000000000..f3cddfd96 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts @@ -0,0 +1,27 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum HCVaultConnectionMethod { + AccessToken = "access-token", + AppRole = "app-role" +} + +export type THCVaultConnection = TRootAppConnection & { app: AppConnection.HCVault } & ( + | { + method: HCVaultConnectionMethod.AccessToken; + credentials: { + instanceUrl: string; + namespace?: string; + accessToken: string; + }; + } + | { + method: HCVaultConnectionMethod.AppRole; + credentials: { + instanceUrl: string; + namespace?: string; + roleId: string; + secretId: string; + }; + } + ); diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index d441e26e0..00c0c3f3a 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -9,6 +9,7 @@ import { TCamundaConnection } from "./camunda-connection"; import { TDatabricksConnection } from "./databricks-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; +import { THCVaultConnection } from "./hc-vault-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; @@ -27,6 +28,7 @@ export * from "./camunda-connection"; export * from "./databricks-connection"; export * from "./gcp-connection"; export * from "./github-connection"; +export * from "./hc-vault-connection"; export * from "./humanitec-connection"; export * from "./ldap-connection"; export * from "./mssql-connection"; @@ -52,6 +54,7 @@ export type TAppConnection = | TCamundaConnection | TWindmillConnection | TAuth0Connection + | THCVaultConnection | TLdapConnection | TTeamCityConnection; @@ -96,6 +99,7 @@ export type TAppConnectionMap = { [AppConnection.Camunda]: TCamundaConnection; [AppConnection.Windmill]: TWindmillConnection; [AppConnection.Auth0]: TAuth0Connection; + [AppConnection.HCVault]: THCVaultConnection; [AppConnection.LDAP]: TLdapConnection; [AppConnection.TeamCity]: TTeamCityConnection; }; 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/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index 32610c28d..cbd20b643 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -107,6 +107,7 @@ export type CompleteAccountSignupDTO = CompleteAccountDTO & { providerAuthToken?: string; attributionSource?: string; organizationName: string; + useDefaultOrg?: boolean; }; export type VerifySignupInviteDTO = { diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 450df773a..d078765bc 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -11,6 +11,7 @@ export enum SecretSync { Camunda = "camunda", Vercel = "vercel", Windmill = "windmill", + HCVault = "hashicorp-vault", TeamCity = "teamcity" } diff --git a/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts b/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts new file mode 100644 index 000000000..388698ef8 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type THCVaultSync = TRootSecretSync & { + destination: SecretSync.HCVault; + destinationConfig: { + mount: string; + path: string; + }; + connection: { + app: AppConnection.HCVault; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index e9ac538fe..2dba65649 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -9,6 +9,7 @@ import { TCamundaSync } from "./camunda-sync"; import { TDatabricksSync } from "./databricks-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; +import { THCVaultSync } from "./hc-vault-sync"; import { THumanitecSync } from "./humanitec-sync"; import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; @@ -34,6 +35,7 @@ export type TSecretSync = | TCamundaSync | TVercelSync | TWindmillSync + | THCVaultSync | TTeamCitySync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; 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 fe09fd7f9..b841f4bff 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 6a20fd9d5..441b9aefa 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -21,7 +21,10 @@ import { TSshHost } from "../sshHost/types"; import { TSshHostGroup } from "../sshHostGroup/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, @@ -899,13 +902,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 e20a5abcc..91fe95a04 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/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index a9242c252..af93b3c2a 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -235,7 +235,7 @@ export const OverviewPage = () => { Default organization
- Select the default organization you want to set for SAML/LDAP/OIDC based + Select the default organization you want to set for SAML/LDAP/OIDC/Github logins. When selected, user logins will be automatically scoped to the selected organization.
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/auth/SignUpSsoPage/SignUpSsoPage.tsx b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx index 40075ae54..6cb4da82b 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx @@ -13,6 +13,7 @@ export const SignupSsoPage = () => { const { t } = useTranslation(); const search = useSearch({ from: ROUTE_PATHS.Auth.SignUpSsoPage.id }); const token = search.token as string; + const defaultOrgAllowed = search.defaultOrgAllowed as boolean | undefined; const [step, setStep] = useState(0); const [password, setPassword] = useState(""); @@ -57,6 +58,7 @@ export const SignupSsoPage = () => { password={password} setPassword={setPassword} providerAuthToken={token} + forceDefaultOrg={defaultOrgAllowed} /> ); default: diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx index a6ab0623b..2f2ade8e7 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -30,6 +30,7 @@ type Props = { name: string; providerOrganizationName: string; providerAuthToken?: string; + forceDefaultOrg?: boolean; }; /** @@ -51,7 +52,8 @@ export const UserInfoSSOStep = ({ providerOrganizationName, password, setPassword, - providerAuthToken + providerAuthToken, + forceDefaultOrg }: Props) => { const [nameError, setNameError] = useState(false); const [organizationName, setOrganizationName] = useState(""); @@ -84,7 +86,7 @@ export const UserInfoSSOStep = ({ } else { setNameError(false); } - if (!organizationName) { + if (!organizationName && !forceDefaultOrg) { setOrganizationNameError(true); errorCheck = true; } else { @@ -160,7 +162,8 @@ export const UserInfoSSOStep = ({ salt: result.salt, verifier: result.verifier, organizationName, - attributionSource + attributionSource, + useDefaultOrg: forceDefaultOrg }); // unset signup JWT token and set JWT token @@ -267,7 +270,7 @@ export const UserInfoSSOStep = ({

)}
- {providerOrganizationName === undefined && ( + {!forceDefaultOrg && providerOrganizationName === undefined && (

Organization Name @@ -279,7 +282,7 @@ export const UserInfoSSOStep = ({ isRequired className="h-12" maxLength={64} - disabled + isDisabled={forceDefaultOrg} /> {organizationNameError && (

diff --git a/frontend/src/pages/auth/SignUpSsoPage/route.tsx b/frontend/src/pages/auth/SignUpSsoPage/route.tsx index a255efaf6..986735ad0 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/route.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/route.tsx @@ -5,7 +5,8 @@ import { z } from "zod"; import { SignupSsoPage } from "./SignUpSsoPage"; const SignupSSOPageQueryParamsSchema = z.object({ - token: z.string() + token: z.string(), + defaultOrgAllowed: z.boolean().optional() }); export const Route = createFileRoute("/_restrict-login-signup/signup/sso")({ diff --git a/frontend/src/pages/middlewares/restrict-login-signup.tsx b/frontend/src/pages/middlewares/restrict-login-signup.tsx index 97ffff752..f85ea44f2 100644 --- a/frontend/src/pages/middlewares/restrict-login-signup.tsx +++ b/frontend/src/pages/middlewares/restrict-login-signup.tsx @@ -97,7 +97,11 @@ export const Route = createFileRoute("/_restrict-login-signup")({ } if (!data.organizationId) { - if (location.pathname.endsWith("select-organization")) return; + if ( + location.pathname.endsWith("select-organization") || + location.pathname.endsWith("verify-email") + ) + return; throw redirect({ to: "/login/select-organization" }); } throw redirect({ diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index fc4477813..5c004ce96 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -18,6 +18,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; +import { HCVaultConnectionForm } from "./HCVaultConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { LdapConnectionForm } from "./LdapConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; @@ -94,6 +95,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.HCVault: + return ; case AppConnection.LDAP: return ; case AppConnection.TeamCity: @@ -164,6 +167,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.HCVault: + return ; case AppConnection.LDAP: return ; case AppConnection.TeamCity: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx new file mode 100644 index 000000000..af3954b45 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx @@ -0,0 +1,224 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: THCVaultConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.HCVault) +}); + +const InstanceUrlSchema = z + .string() + .trim() + .min(1, "Instance URL required") + .url("Invalid Instance URL"); + +const NamespaceSchema = z.string().trim().optional(); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AccessToken), + credentials: z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + accessToken: z.string().trim().min(1, "Access Token required") + }) + }), + rootSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AppRole), + credentials: z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + roleId: z.string().trim().min(1, "Role ID required"), + secretId: z.string().trim().min(1, "Secret ID required") + }) + }) +]); + +type FormData = z.infer; + +export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.HCVault, + method: HCVaultConnectionMethod.AppRole + } + }); + + const { + handleSubmit, + control, + watch, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + + return ( + +

+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {selectedMethod === HCVaultConnectionMethod.AccessToken ? ( + ( + + + + )} + /> + ) : ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + + )} +
+ + + + +
+ + + ); +}; 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 + + )} + + )} + { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 776f4665e..abfbf100c 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -8,6 +8,7 @@ import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; +import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol"; import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; @@ -44,6 +45,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Windmill: return ; + case SecretSync.HCVault: + return ; case SecretSync.TeamCity: return ; default: diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 27dc9d5a1..b1fe387e5 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -94,6 +94,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.workspace; secondaryText = destinationConfig.path; break; + case SecretSync.HCVault: + primaryText = destinationConfig.mount; + secondaryText = destinationConfig.path; + break; case SecretSync.TeamCity: primaryText = destinationConfig.project; secondaryText = destinationConfig.buildConfig; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx new file mode 100644 index 000000000..423e56cf0 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx @@ -0,0 +1,19 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync"; + +type Props = { + secretSync: THCVaultSync; +}; + +export const HCVaultSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { path, mount } + } = secretSync; + + return ( + <> + {mount} + {path} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 505f3ac3b..4ea5798d9 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -18,6 +18,7 @@ import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; +import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection"; import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; @@ -35,7 +36,7 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: const app = APP_CONNECTION_MAP[connection.app].name; let DestinationComponents: ReactNode; - switch (secretSync.destination) { + switch (destination) { case SecretSync.AWSParameterStore: DestinationComponents = ; break; @@ -74,6 +75,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Windmill: DestinationComponents = ; break; + case SecretSync.HCVault: + DestinationComponents = ; + break; case SecretSync.TeamCity: DestinationComponents = ; break; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index bc6713d3d..5995e7cd1 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Camunda: case SecretSync.Vercel: case SecretSync.Windmill: + case SecretSync.HCVault: case SecretSync.TeamCity: AdditionalSyncOptionsComponent = null; break; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx index d5510c619..c3e2a20f1 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/WorkflowIntegrationSection/WorkflowIntegrationTab.tsx @@ -1,351 +1,163 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; +import { BsMicrosoftTeams, BsSlack } from "react-icons/bs"; +import { faGear, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Link } from "@tanstack/react-router"; -import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; +import { OrgPermissionCan } from "@app/components/permissions"; import { Button, - ContentLoader, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DeleteActionModal, EmptyState, - FormControl, - Input, - Select, - SelectItem, - Switch + Table, + TableContainer, + TBody, + Td, + THead, + Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; import { - useGetSlackIntegrationChannels, - useGetSlackIntegrations, - useGetWorkspaceSlackConfig, - useUpdateProjectSlackConfig + useDeleteProjectWorkflowIntegration, + useGetWorkspaceWorkflowIntegrationConfig } 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() -}); +import { AddWorkflowIntegrationModal } from "./components/AddWorkflowIntegrationModal"; +import { EditWorkflowIntegrationModal } from "./components/EditWorkflowIntegrationModal"; +import { MicrosoftTeamsConfigRow } from "./components/MicrosoftTeamsConfigRow"; +import { SlackConfigRow } from "./components/SlackConfigRow"; -type TSlackConfigForm = z.infer; +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 1f209bd5e..15d67ab46 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' @@ -218,6 +219,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', @@ -484,6 +489,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', @@ -522,14 +535,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', @@ -671,6 +676,14 @@ const organizationCertManagerOverviewPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) +const organizationSettingsPageRouteRoute = + organizationSettingsPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute, + } as any) + const organizationSecretSharingPageRouteRoute = organizationSecretSharingPageRouteImport.update({ id: '/', @@ -865,6 +878,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', @@ -1960,13 +1981,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' @@ -2002,6 +2016,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' @@ -2037,6 +2058,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' @@ -2177,6 +2205,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' @@ -3105,16 +3140,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 @@ -3134,13 +3186,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, organizationSecretScanningPageRouteRoute: organizationSecretScanningPageRouteRoute, - organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren, + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: + AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren, organizationCertManagerOverviewPageRouteRoute: organizationCertManagerOverviewPageRouteRoute, organizationGroupDetailsByIDPageRouteRoute: @@ -3827,17 +3880,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 @@ -3854,6 +3908,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 @@ -4008,7 +4063,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 @@ -4016,6 +4070,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 @@ -4032,6 +4087,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 @@ -4195,17 +4251,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 @@ -4226,6 +4283,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 @@ -4386,17 +4444,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' @@ -4413,6 +4472,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' @@ -4566,7 +4626,6 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/secret-scanning' - | '/organization/settings' | '/cert-manager/$projectId' | '/kms/$projectId' | '/secret-manager/$projectId' @@ -4574,6 +4633,7 @@ export interface FileRouteTypes { | '/organization/app-connections' | '/organization/gateways' | '/organization/secret-sharing' + | '/organization/settings' | '/organization/cert-manager/overview' | '/organization/groups/$groupId' | '/organization/identities/$identityId' @@ -4590,6 +4650,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' @@ -4751,17 +4812,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' @@ -4782,6 +4844,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' @@ -5141,10 +5204,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", @@ -5186,10 +5249,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", @@ -5227,6 +5286,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", @@ -5253,6 +5320,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" @@ -5379,6 +5450,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 f33a71de7..3d9889ab3 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"),