Merge pull request #3486 from Infisical/daniel/ms-teams-integration

feat(workflow-integrations): microsoft teams
This commit is contained in:
Daniel Hougaard
2025-05-02 00:46:19 +04:00
committed by GitHub
104 changed files with 6874 additions and 1013 deletions
+2
View File
@@ -24,3 +24,5 @@ frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
docs/documentation/platform/kms/overview.mdx:generic-api-key:281 docs/documentation/platform/kms/overview.mdx:generic-api-key:281
docs/documentation/platform/kms/overview.mdx:generic-api-key:344 docs/documentation/platform/kms/overview.mdx:generic-api-key:344
docs/cli/commands/user.mdx:generic-api-key:51
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
+598 -77
View File
@@ -59,6 +59,7 @@
"axios": "^1.6.7", "axios": "^1.6.7",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
"bullmq": "^5.4.2", "bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2", "cassandra-driver": "^4.7.2",
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
@@ -2358,12 +2359,13 @@
} }
}, },
"node_modules/@azure/core-auth": { "node_modules/@azure/core-auth": {
"version": "1.7.2", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
"integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.0.0", "@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.1.0", "@azure/core-util": "^1.11.0",
"tslib": "^2.6.2" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
@@ -2518,14 +2520,15 @@
} }
}, },
"node_modules/@azure/core-rest-pipeline": { "node_modules/@azure/core-rest-pipeline": {
"version": "1.16.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.1.tgz", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz",
"integrity": "sha512-ExPSbgjwCoht6kB7B4MeZoBAxcQSIl29r/bPeazZJx50ej4JJCByimLOrZoIsurISNyJQQHf30b3JfqC3Hb88A==", "integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.0.0", "@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-tracing": "^1.0.1",
"@azure/core-util": "^1.9.0", "@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0", "@azure/logger": "^1.0.0",
"http-proxy-agent": "^7.0.0", "http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0",
@@ -2602,9 +2605,10 @@
} }
}, },
"node_modules/@azure/core-util": { "node_modules/@azure/core-util": {
"version": "1.9.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
"integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.0.0", "@azure/abort-controller": "^2.0.0",
"tslib": "^2.6.2" "tslib": "^2.6.2"
@@ -2625,46 +2629,60 @@
} }
}, },
"node_modules/@azure/identity": { "node_modules/@azure/identity": {
"version": "4.3.0", "version": "4.9.1",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.3.0.tgz", "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz",
"integrity": "sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==", "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^1.0.0", "@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.5.0", "@azure/core-auth": "^1.9.0",
"@azure/core-client": "^1.9.2", "@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-tracing": "^1.0.0",
"@azure/core-util": "^1.3.0", "@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0", "@azure/logger": "^1.0.0",
"@azure/msal-browser": "^3.11.1", "@azure/msal-browser": "^4.2.0",
"@azure/msal-node": "^2.9.2", "@azure/msal-node": "^3.5.0",
"events": "^3.0.0", "open": "^10.1.0",
"jws": "^4.0.0",
"open": "^8.0.0",
"stoppable": "^1.1.0",
"tslib": "^2.2.0" "tslib": "^2.2.0"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@azure/identity/node_modules/jwa": { "node_modules/@azure/identity/node_modules/@azure/abort-controller": {
"version": "2.0.0", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
"license": "MIT",
"dependencies": { "dependencies": {
"buffer-equal-constant-time": "1.0.1", "tslib": "^2.6.2"
"ecdsa-sig-formatter": "1.0.11", },
"safe-buffer": "^5.0.1" "engines": {
"node": ">=18.0.0"
} }
}, },
"node_modules/@azure/identity/node_modules/jws": { "node_modules/@azure/identity/node_modules/@azure/msal-node": {
"version": "4.0.0", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==",
"license": "MIT",
"dependencies": { "dependencies": {
"jwa": "^2.0.0", "@azure/msal-common": "15.5.1",
"safe-buffer": "^5.0.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": { "node_modules/@azure/keyvault-keys": {
@@ -2700,30 +2718,33 @@
} }
}, },
"node_modules/@azure/msal-browser": { "node_modules/@azure/msal-browser": {
"version": "3.18.0", "version": "4.11.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.18.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz",
"integrity": "sha512-jvK5bDUWbpOaJt2Io/rjcaOVcUzkqkrCme/WntdV1SMUc67AiTcEdKuY6G/nMQ7N5Cfsk9SfpugflQwDku53yg==", "integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@azure/msal-common": "14.13.0" "@azure/msal-common": "15.5.1"
}, },
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/@azure/msal-common": { "node_modules/@azure/msal-common": {
"version": "14.13.0", "version": "15.5.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.13.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz",
"integrity": "sha512-b4M/tqRzJ4jGU91BiwCsLTqChveUEyFK3qY2wGfZ0zBswIBZjAxopx5CYt5wzZFKuN15HqRDYXQbztttuIC3nA==", "integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/@azure/msal-node": { "node_modules/@azure/msal-node": {
"version": "2.10.0", "version": "2.16.2",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.10.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz",
"integrity": "sha512-JxsSE0464a8IA/+q5EHKmchwNyUFJHtCH00tSXsLaOddwLjG6yVvTH6lGgPcWMhO7YWUXj/XVgVgeE9kZtsPUQ==", "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@azure/msal-common": "14.13.0", "@azure/msal-common": "14.16.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0" "uuid": "^8.3.0"
}, },
@@ -2731,6 +2752,15 @@
"node": ">=16" "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": { "node_modules/@azure/msal-node/node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -10011,9 +10041,10 @@
"dev": true "dev": true
}, },
"node_modules/@types/jsonwebtoken": { "node_modules/@types/jsonwebtoken": {
"version": "9.0.5", "version": "9.0.6",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz",
"integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -10398,6 +10429,15 @@
"@types/webidl-conversions": "*" "@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": { "node_modules/@types/xml-encryption": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz", "resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz",
@@ -11150,6 +11190,12 @@
"node": ">=0.4.0" "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": { "node_modules/adm-zip": {
"version": "0.5.12", "version": "0.5.12",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
@@ -12012,6 +12058,245 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "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": { "node_modules/bottleneck": {
"version": "2.19.5", "version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
@@ -12139,6 +12424,21 @@
"uuid": "^9.0.0" "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": { "node_modules/bundle-require": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz",
@@ -12791,6 +13091,15 @@
"node": ">=12.0.0" "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -12872,6 +13181,12 @@
"node": "*" "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": { "node_modules/dc-polyfill": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz", "resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz",
@@ -13011,6 +13326,34 @@
"node": ">=0.10.0" "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": { "node_modules/define-data-property": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -13029,11 +13372,15 @@
} }
}, },
"node_modules/define-lazy-prop": { "node_modules/define-lazy-prop": {
"version": "2.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/define-properties": { "node_modules/define-properties": {
@@ -13093,6 +13440,15 @@
"node": ">= 0.8" "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": { "node_modules/deprecation": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
@@ -13166,6 +13522,47 @@
"node": ">=6.0.0" "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": { "node_modules/dompurify": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
@@ -13174,6 +13571,20 @@
"@types/trusted-types": "^2.0.7" "@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": { "node_modules/dotenv": {
"version": "16.4.1", "version": "16.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
@@ -14494,6 +14905,33 @@
"node": "^10.12.0 || >=12.0.0" "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": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -15678,6 +16116,25 @@
], ],
"license": "MIT" "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": { "node_modules/http-cache-semantics": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -16191,6 +16648,39 @@
"node": ">=0.10.0" "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": { "node_modules/is-interactive": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
@@ -16718,7 +17208,6 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -18767,16 +19256,33 @@
} }
}, },
"node_modules/open": { "node_modules/open": {
"version": "8.4.2", "version": "10.1.1",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==",
"license": "MIT",
"dependencies": { "dependencies": {
"define-lazy-prop": "^2.0.0", "default-browser": "^5.2.1",
"is-docker": "^2.1.1", "define-lazy-prop": "^3.0.0",
"is-wsl": "^2.2.0" "is-inside-container": "^1.0.0",
"is-wsl": "^3.1.0"
}, },
"engines": { "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": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -18801,6 +19307,12 @@
"url": "https://github.com/sponsors/panva" "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": { "node_modules/opentracing": {
"version": "0.14.7", "version": "0.14.7",
"resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz", "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", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==" "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": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -21728,15 +22258,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/stream-events": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
@@ -23520,7 +24041,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -24951,9 +25471,10 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.22.4", "version": "3.24.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
"license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+1
View File
@@ -175,6 +175,7 @@
"axios": "^1.6.7", "axios": "^1.6.7",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
"bullmq": "^5.4.2", "bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2", "cassandra-driver": "^4.7.2",
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
+2
View File
@@ -100,6 +100,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-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" { declare module "@fastify/request-context" {
interface RequestContextData { interface RequestContextData {
@@ -246,6 +247,7 @@ declare module "fastify" {
kmipOperation: TKmipOperationServiceFactory; kmipOperation: TKmipOperationServiceFactory;
gateway: TGatewayServiceFactory; gateway: TGatewayServiceFactory;
secretRotationV2: TSecretRotationV2ServiceFactory; secretRotationV2: TSecretRotationV2ServiceFactory;
microsoftTeams: TMicrosoftTeamsServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory; assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory; githubOrgSync: TGithubOrgSyncServiceFactory;
}; };
+20
View File
@@ -426,6 +426,16 @@ import {
TWorkflowIntegrationsInsert, TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate TWorkflowIntegrationsUpdate
} from "@app/db/schemas"; } 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 { import {
TSecretReminderRecipients, TSecretReminderRecipients,
TSecretReminderRecipientsInsert, TSecretReminderRecipientsInsert,
@@ -1002,6 +1012,16 @@ declare module "knex/types/tables" {
TSecretRotationV2SecretMappingsInsert, TSecretRotationV2SecretMappingsInsert,
TSecretRotationV2SecretMappingsUpdate TSecretRotationV2SecretMappingsUpdate
>; >;
[TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType<
TMicrosoftTeamsIntegrations,
TMicrosoftTeamsIntegrationsInsert,
TMicrosoftTeamsIntegrationsUpdate
>;
[TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType<
TProjectMicrosoftTeamsConfigs,
TProjectMicrosoftTeamsConfigsInsert,
TProjectMicrosoftTeamsConfigsUpdate
>;
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType< [TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
TSecretReminderRecipients, TSecretReminderRecipients,
TSecretReminderRecipientsInsert, TSecretReminderRecipientsInsert,
@@ -0,0 +1,130 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
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<void> {
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);
}
}
+1
View File
@@ -58,6 +58,7 @@ export * from "./kms-keys";
export * from "./kms-root-config"; export * from "./kms-root-config";
export * from "./ldap-configs"; export * from "./ldap-configs";
export * from "./ldap-group-maps"; export * from "./ldap-group-maps";
export * from "./microsoft-teams-integrations";
export * from "./models"; export * from "./models";
export * from "./oidc-configs"; export * from "./oidc-configs";
export * from "./org-bots"; export * from "./org-bots";
@@ -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<typeof MicrosoftTeamsIntegrationsSchema>;
export type TMicrosoftTeamsIntegrationsInsert = Omit<
z.input<typeof MicrosoftTeamsIntegrationsSchema>,
TImmutableDBKeys
>;
export type TMicrosoftTeamsIntegrationsUpdate = Partial<
Omit<z.input<typeof MicrosoftTeamsIntegrationsSchema>, TImmutableDBKeys>
>;
+2
View File
@@ -147,6 +147,8 @@ export enum TableName {
KmipClientCertificates = "kmip_client_certificates", KmipClientCertificates = "kmip_client_certificates",
SecretRotationV2 = "secret_rotations_v2", SecretRotationV2 = "secret_rotations_v2",
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings", SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
SecretReminderRecipients = "secret_reminder_recipients", SecretReminderRecipients = "secret_reminder_recipients",
GithubOrgSyncConfig = "github_org_sync_configs" GithubOrgSyncConfig = "github_org_sync_configs"
} }
@@ -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<typeof ProjectMicrosoftTeamsConfigsSchema>;
export type TProjectMicrosoftTeamsConfigsInsert = Omit<
z.input<typeof ProjectMicrosoftTeamsConfigsSchema>,
TImmutableDBKeys
>;
export type TProjectMicrosoftTeamsConfigsUpdate = Partial<
Omit<z.input<typeof ProjectMicrosoftTeamsConfigsSchema>, TImmutableDBKeys>
>;
+4 -1
View File
@@ -26,7 +26,10 @@ export const SuperAdminSchema = z.object({
encryptedSlackClientSecret: zodBuffer.nullable().optional(), encryptedSlackClientSecret: zodBuffer.nullable().optional(),
authConsentContent: z.string().nullable().optional(), authConsentContent: z.string().nullable().optional(),
pageFrameContent: 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<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
@@ -14,7 +14,8 @@ export const WorkflowIntegrationsSchema = z.object({
orgId: z.string().uuid(), orgId: z.string().uuid(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
status: z.string().default("installed")
}); });
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>; export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
@@ -6,13 +6,15 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid"; 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 { 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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -67,6 +69,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
>; >;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">; projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
}; };
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>; export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
@@ -84,6 +88,8 @@ export const accessApprovalRequestServiceFactory = ({
smtpService, smtpService,
userDAL, userDAL,
kmsService, kmsService,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL,
projectSlackConfigDAL projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const createAccessApprovalRequest = async ({ const createAccessApprovalRequest = async ({
@@ -219,24 +225,30 @@ export const accessApprovalRequestServiceFactory = ({
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`; const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
await triggerSlackNotification({ await triggerWorkflowIntegrationNotification({
projectId: project.id, input: {
projectSlackConfigDAL, notification: {
projectDAL, type: TriggerFeature.ACCESS_REQUEST,
kmsService, payload: {
notification: { projectName: project.name,
type: SlackTriggerFeature.ACCESS_REQUEST, requesterFullName,
payload: { isTemporary,
projectName: project.name, requesterEmail: requestedByUser.email as string,
requesterFullName, secretPath,
isTemporary, environment: envSlug,
requesterEmail: requestedByUser.email as string, permissions: accessTypes,
secretPath, approvalUrl,
environment: envSlug, note
permissions: accessTypes, }
approvalUrl, },
note projectId: project.id
} },
dependencies: {
projectDAL,
projectSlackConfigDAL,
kmsService,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL
} }
}); });
@@ -29,6 +29,7 @@ import {
TSecretSyncRaw, TSecretSyncRaw,
TUpdateSecretSyncDTO TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types"; } 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 { KmipPermission } from "../kmip/kmip-enum";
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
@@ -244,11 +245,14 @@ export enum EventType {
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-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", GET_SLACK_INTEGRATION = "get-slack-integration",
UPDATE_SLACK_INTEGRATION = "update-slack-integration", UPDATE_SLACK_INTEGRATION = "update-slack-integration",
DELETE_SLACK_INTEGRATION = "delete-slack-integration", DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config", GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config", UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
INTEGRATION_SYNCED = "integration-synced", INTEGRATION_SYNCED = "integration-synced",
@@ -321,6 +325,15 @@ export enum EventType {
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets", SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
PROJECT_ACCESS_REQUEST = "project-access-request", 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_START = "project-assume-privileges-session-start",
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end" PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
} }
@@ -1980,22 +1993,24 @@ interface GetSlackIntegration {
}; };
} }
interface UpdateProjectSlackConfig { interface UpdateProjectWorkflowIntegrationConfig {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG; type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: { metadata: {
id: string; id: string;
slackIntegrationId: string; integrationId: string;
integration: WorkflowIntegration;
isAccessRequestNotificationEnabled: boolean; isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string; accessRequestChannels?: string | { teamId: string; channelIds: string[] };
isSecretRequestNotificationEnabled: boolean; isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string; secretRequestChannels?: string | { teamId: string; channelIds: string[] };
}; };
} }
interface GetProjectSlackConfig { interface GetProjectWorkflowIntegrationConfig {
type: EventType.GET_PROJECT_SLACK_CONFIG; type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: { metadata: {
id: string; id: string;
integration: WorkflowIntegration;
}; };
} }
@@ -2561,6 +2576,66 @@ interface RotateSecretRotationEvent {
}; };
} }
interface MicrosoftTeamsWorkflowIntegrationCreateEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE;
metadata: {
tenantId: string;
slug: string;
description?: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationDeleteEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE;
metadata: {
tenantId: string;
id: string;
slug: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS;
metadata: {
tenantId: string;
slug: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS;
metadata: {
tenantId: string;
slug: string;
id: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationGetEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET;
metadata: {
tenantId: string;
slug: string;
id: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationListEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST;
metadata: Record<string, string>;
}
interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE;
metadata: {
tenantId: string;
slug: string;
id: string;
newSlug?: string;
newDescription?: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -2723,8 +2798,8 @@ export type Event =
| UpdateSlackIntegration | UpdateSlackIntegration
| DeleteSlackIntegration | DeleteSlackIntegration
| GetSlackIntegration | GetSlackIntegration
| UpdateProjectSlackConfig | UpdateProjectWorkflowIntegrationConfig
| GetProjectSlackConfig | GetProjectWorkflowIntegrationConfig
| GetProjectSshConfig | GetProjectSshConfig
| UpdateProjectSshConfig | UpdateProjectSshConfig
| IntegrationSyncedEvent | IntegrationSyncedEvent
@@ -2794,4 +2869,11 @@ export type Event =
| CreateSecretRotationEvent | CreateSecretRotationEvent
| UpdateSecretRotationEvent | UpdateSecretRotationEvent
| DeleteSecretRotationEvent | DeleteSecretRotationEvent
| RotateSecretRotationEvent; | RotateSecretRotationEvent
| MicrosoftTeamsWorkflowIntegrationCreateEvent
| MicrosoftTeamsWorkflowIntegrationDeleteEvent
| MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
| MicrosoftTeamsWorkflowIntegrationGetEvent
| MicrosoftTeamsWorkflowIntegrationListEvent
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
@@ -17,9 +17,13 @@ import { groupBy, pick, unique } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex"; import { setKnexStringValue } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types"; 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 { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; 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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; 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 { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -126,6 +128,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">; secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">; projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
}; };
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>; export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -155,7 +159,9 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
licenseService, licenseService,
projectSlackConfigDAL, projectSlackConfigDAL,
resourceMetadataDAL resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); 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 env = await projectEnvDAL.findOne({ id: policy.envId });
const user = await userDAL.findById(secretApprovalRequest.committerUserId); const user = await userDAL.findById(secretApprovalRequest.committerUserId);
await triggerSlackNotification({
projectId, await triggerWorkflowIntegrationNotification({
projectDAL, input: {
kmsService, projectId,
projectSlackConfigDAL, notification: {
notification: { type: TriggerFeature.SECRET_APPROVAL,
type: SlackTriggerFeature.SECRET_APPROVAL, payload: {
payload: { userEmail: user.email as string,
userEmail: user.email as string, environment: env.name,
environment: env.name, secretPath,
secretPath, projectId,
projectId, requestId: secretApprovalRequest.id,
requestId: secretApprovalRequest.id, secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
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 user = await userDAL.findById(secretApprovalRequest.committerUserId);
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
await triggerSlackNotification({
projectId, await triggerWorkflowIntegrationNotification({
projectDAL, input: {
kmsService, projectId,
projectSlackConfigDAL, notification: {
notification: { type: TriggerFeature.SECRET_APPROVAL,
type: SlackTriggerFeature.SECRET_APPROVAL, payload: {
payload: { userEmail: user.email as string,
userEmail: user.email as string, environment: env.name,
environment: env.name, secretPath,
secretPath, projectId,
projectId, requestId: secretApprovalRequest.id,
requestId: secretApprovalRequest.id, secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))] }
} }
},
dependencies: {
projectDAL,
kmsService,
projectSlackConfigDAL,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL
} }
}); });
@@ -20,7 +20,7 @@ export const BaseSecretRotationSchema = (type: SecretRotation) =>
// unique to provider // unique to provider
type: true, type: true,
parameters: true, parameters: true,
secretMappings: true secretsMapping: true
}).extend({ }).extend({
connection: z.object({ connection: z.object({
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]), app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
@@ -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");
}
};
@@ -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<TProjectDALFactory, "findById">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
};
};
@@ -111,6 +111,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
return; 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); const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
if (!authMode) return; if (!authMode) return;
+26 -3
View File
@@ -174,6 +174,9 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
import { kmsServiceFactory } from "@app/services/kms/kms-service"; 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 { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
import { orgBotDALFactory } from "@app/services/org/org-bot-dal"; import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
import { orgDALFactory } from "@app/services/org/org-dal"; import { orgDALFactory } from "@app/services/org/org-dal";
@@ -426,6 +429,8 @@ export const registerRoutes = async (
const githubOrgSyncDAL = githubOrgSyncDALFactory(db); const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL); const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@@ -687,6 +692,15 @@ export const registerRoutes = async (
orgDAL, orgDAL,
externalGroupOrgRoleMappingDAL externalGroupOrgRoleMappingDAL
}); });
const microsoftTeamsService = microsoftTeamsServiceFactory({
microsoftTeamsIntegrationDAL,
permissionService,
workflowIntegrationDAL,
kmsService,
serverCfgDAL: superAdminDAL
});
const superAdminService = superAdminServiceFactory({ const superAdminService = superAdminServiceFactory({
userDAL, userDAL,
identityDAL, identityDAL,
@@ -700,7 +714,8 @@ export const registerRoutes = async (
orgService, orgService,
keyStore, keyStore,
licenseService, licenseService,
kmsService kmsService,
microsoftTeamsService
}); });
const orgAdminService = orgAdminServiceFactory({ const orgAdminService = orgAdminServiceFactory({
@@ -1026,6 +1041,8 @@ export const registerRoutes = async (
certificateTemplateDAL, certificateTemplateDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsIntegrationDAL,
projectTemplateService, projectTemplateService,
groupProjectDAL, groupProjectDAL,
smtpService smtpService
@@ -1150,7 +1167,9 @@ export const registerRoutes = async (
userDAL, userDAL,
licenseService, licenseService,
projectSlackConfigDAL, projectSlackConfigDAL,
resourceMetadataDAL resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
}); });
const secretService = secretServiceFactory({ const secretService = secretServiceFactory({
@@ -1212,7 +1231,9 @@ export const registerRoutes = async (
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
kmsService, kmsService,
groupDAL groupDAL,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL
}); });
const secretReplicationService = secretReplicationServiceFactory({ const secretReplicationService = secretReplicationServiceFactory({
@@ -1610,6 +1631,7 @@ export const registerRoutes = async (
await dailyResourceCleanUp.startCleanUp(); await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts(); await dailyExpiringPkiItemAlert.startSendingAlerts();
await kmsService.startService(); await kmsService.startService();
await microsoftTeamsService.start();
// inject all services // inject all services
server.decorate<FastifyZodProvider["services"]>("services", { server.decorate<FastifyZodProvider["services"]>("services", {
@@ -1702,6 +1724,7 @@ export const registerRoutes = async (
kmipOperation: kmipOperationService, kmipOperation: kmipOperationService,
gateway: gatewayService, gateway: gatewayService,
secretRotationV2: secretRotationV2Service, secretRotationV2: secretRotationV2Service,
microsoftTeams: microsoftTeamsService,
assumePrivileges: assumePrivilegeService, assumePrivileges: assumePrivilegeService,
githubOrgSync: githubOrgSyncConfigService githubOrgSync: githubOrgSyncConfigService
}); });
+19 -6
View File
@@ -27,7 +27,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
encryptedSlackClientId: true, encryptedSlackClientId: true,
encryptedSlackClientSecret: true encryptedSlackClientSecret: true,
encryptedMicrosoftTeamsAppId: true,
encryptedMicrosoftTeamsClientSecret: true,
encryptedMicrosoftTeamsBotId: true
}).extend({ }).extend({
isMigrationModeOn: z.boolean(), isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgSlug: z.string().nullable(),
@@ -74,6 +77,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}), }),
slackClientId: z.string().optional(), slackClientId: z.string().optional(),
slackClientSecret: z.string().optional(), slackClientSecret: z.string().optional(),
microsoftTeamsAppId: z.string().optional(),
microsoftTeamsClientSecret: z.string().optional(),
microsoftTeamsBotId: z.string().optional(),
authConsentContent: z authConsentContent: z
.string() .string()
.trim() .trim()
@@ -197,15 +203,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/integrations/slack/config", url: "/integrations",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
clientId: z.string(), slack: z.object({
clientSecret: z.string() 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 () => { handler: async () => {
const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig(); const adminIntegrationsConfig = await server.services.superAdmin.getAdminIntegrationsConfig();
return adminSlackConfig; return adminIntegrationsConfig;
} }
}); });
+2
View File
@@ -47,6 +47,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router";
import { registerUserRouter } from "./user-router"; import { registerUserRouter } from "./user-router";
import { registerWebhookRouter } from "./webhook-router"; import { registerWebhookRouter } from "./webhook-router";
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
export const registerV1Routes = async (server: FastifyZodProvider) => { export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" }); await server.register(registerSsoRouter, { prefix: "/sso" });
@@ -79,6 +80,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (workflowIntegrationRouter) => { async (workflowIntegrationRouter) => {
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter); await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" }); await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
await workflowIntegrationRouter.register(registerMicrosoftTeamsRouter, { prefix: "/microsoft-teams" });
}, },
{ prefix: "/workflow-integrations" } { prefix: "/workflow-integrations" }
); );
@@ -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);
}
});
};
+141 -50
View File
@@ -14,6 +14,7 @@ import {
UserEncryptionKeysSchema, UserEncryptionKeysSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } 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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; 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 { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators"; 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 { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
import { sanitizedServiceTokenSchema } from "../v2/service-token-router"; import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
@@ -740,55 +743,112 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:workspaceId/slack-config", url: "/:workspaceId/workflow-integration-config/:integration",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim(),
integration: z.nativeEnum(WorkflowIntegration)
}), }),
response: { response: {
200: ProjectSlackConfigsSchema.pick({ 200: z.discriminatedUnion("integration", [
id: true, ProjectSlackConfigsSchema.pick({
slackIntegrationId: true, id: true,
isAccessRequestNotificationEnabled: true, isAccessRequestNotificationEnabled: true,
accessRequestChannels: true, accessRequestChannels: true,
isSecretRequestNotificationEnabled: true, isSecretRequestNotificationEnabled: true,
secretRequestChannels: 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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const slackConfig = await server.services.project.getProjectSlackConfig({ const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId projectId: req.params.projectId,
integration: req.params.integration,
integrationId: req.params.integrationId
}); });
if (slackConfig) { return {
await server.services.auditLog.createAuditLog({ integrationConfig: deletedIntegration
...req.auditLogInfo, };
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_SLACK_CONFIG,
metadata: {
id: slackConfig.id
}
}
});
}
return slackConfig;
} }
}); });
server.route({ server.route({
method: "PUT", method: "PUT",
url: "/:workspaceId/slack-config", url: "/:workspaceId/workflow-integration",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -796,27 +856,57 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim()
}), }),
body: z.object({
slackIntegrationId: z.string(), body: z.discriminatedUnion("integration", [
isAccessRequestNotificationEnabled: z.boolean(), z.object({
accessRequestChannels: validateSlackChannelsField, integration: z.literal(WorkflowIntegration.SLACK),
isSecretRequestNotificationEnabled: z.boolean(), integrationId: z.string(),
secretRequestChannels: validateSlackChannelsField accessRequestChannels: validateSlackChannelsField,
}), secretRequestChannels: validateSlackChannelsField,
response: { isAccessRequestNotificationEnabled: z.boolean(),
200: ProjectSlackConfigsSchema.pick({ isSecretRequestNotificationEnabled: z.boolean()
id: true, }),
slackIntegrationId: true, z.object({
isAccessRequestNotificationEnabled: true, integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
accessRequestChannels: true, integrationId: z.string(),
isSecretRequestNotificationEnabled: true, accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
secretRequestChannels: true 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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const slackConfig = await server.services.project.updateProjectSlackConfig({ const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type, actor: req.permission.type,
@@ -829,19 +919,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
event: { event: {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG, type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
metadata: { metadata: {
id: slackConfig.id, id: workflowIntegrationConfig.id,
slackIntegrationId: slackConfig.slackIntegrationId, integrationId: workflowIntegrationConfig.integrationId,
isAccessRequestNotificationEnabled: slackConfig.isAccessRequestNotificationEnabled, integration: workflowIntegrationConfig.integration,
accessRequestChannels: slackConfig.accessRequestChannels, isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled,
isSecretRequestNotificationEnabled: slackConfig.isSecretRequestNotificationEnabled, accessRequestChannels: workflowIntegrationConfig.accessRequestChannels,
secretRequestChannels: slackConfig.secretRequestChannels isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled,
secretRequestChannels: workflowIntegrationConfig.secretRequestChannels
} }
} }
}); });
return slackConfig; return workflowIntegrationConfig;
} }
}); });
@@ -7,7 +7,8 @@ const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({
id: true, id: true,
description: true, description: true,
slug: true, slug: true,
integration: true integration: true,
status: true
}); });
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => { export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {
@@ -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<TKmsServiceFactory, "createCipherPairWithDataKey">;
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
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<TKmsServiceFactory, "createCipherPairWithDataKey">;
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
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<TWorkflowIntegrationDALFactory, "update">;
private microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
constructor({
botAppId,
botAppPassword,
workflowIntegrationDAL,
microsoftTeamsIntegrationDAL
}: {
botAppId: string;
botAppPassword: string;
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
}) {
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"
});
@@ -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<typeof microsoftTeamsIntegrationDALFactory>;
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<TMicrosoftTeamsIntegrations> & Partial<TWorkflowIntegrations>,
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
};
};
@@ -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<string, unknown> {
// 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<string, unknown>;
}
type TMicrosoftTeamsServiceFactoryDep = {
microsoftTeamsIntegrationDAL: Pick<
TMicrosoftTeamsIntegrationDALFactory,
| "deleteById"
| "updateById"
| "create"
| "findOne"
| "findById"
| "findByIdWithWorkflowIntegrationDetails"
| "findWithWorkflowIntegrationDetails"
| "update"
>;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
workflowIntegrationDAL: Pick<
TWorkflowIntegrationDALFactory,
"transaction" | "create" | "updateById" | "deleteById" | "update" | "findOne"
>;
serverCfgDAL: Pick<TSuperAdminDALFactory, "findById">;
};
export type TMicrosoftTeamsServiceFactory = ReturnType<typeof microsoftTeamsServiceFactory>;
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
};
};
@@ -0,0 +1,42 @@
import { TOrgPermission } from "@app/lib/types";
import { TNotification } from "@app/lib/workflow-integrations/types";
export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
export type TGetClientIdDTO = Omit<TOrgPermission, "orgId">;
export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId"> & {
tenantId: string;
slug: string;
redirectUri: string;
description?: string;
code: string;
};
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;
export type TGetMicrosoftTeamsIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
export type TUpdateMicrosoftTeamsIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
TOrgPermission,
"orgId"
>;
export type TGetTeamsDTO = Omit<TOrgPermission, "orgId"> & {
workflowIntegrationId: string;
};
export type TDeleteMicrosoftTeamsIntegrationDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export type TSendNotificationDTO = {
tenantId: string;
microsoftTeamsIntegrationId: string;
orgId: string;
target: {
teamId: string;
channelIds: string[];
};
notification: TNotification;
};
@@ -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<typeof projectMicrosoftTeamsConfigDALFactory>;
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 };
};
+284 -56
View File
@@ -43,6 +43,9 @@ import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TKmsServiceFactory } from "../kms/kms-service"; 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 { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
@@ -60,9 +63,11 @@ import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal"; import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
import { validateSlackChannelsField } from "../slack/slack-auth-validators";
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal"; import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue"; import { TProjectQueueFactory } from "./project-queue";
@@ -70,10 +75,11 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import { import {
TCreateProjectDTO, TCreateProjectDTO,
TDeleteProjectDTO, TDeleteProjectDTO,
TDeleteProjectWorkflowIntegration,
TGetProjectDTO, TGetProjectDTO,
TGetProjectKmsKey, TGetProjectKmsKey,
TGetProjectSlackConfig,
TGetProjectSshConfig, TGetProjectSshConfig,
TGetProjectWorkflowIntegrationConfig,
TListProjectAlertsDTO, TListProjectAlertsDTO,
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertificateTemplatesDTO, TListProjectCertificateTemplatesDTO,
@@ -92,9 +98,9 @@ import {
TUpdateProjectDTO, TUpdateProjectDTO,
TUpdateProjectKmsDTO, TUpdateProjectKmsDTO,
TUpdateProjectNameDTO, TUpdateProjectNameDTO,
TUpdateProjectSlackConfig,
TUpdateProjectSshConfig, TUpdateProjectSshConfig,
TUpdateProjectVersionLimitDTO, TUpdateProjectVersionLimitDTO,
TUpdateProjectWorkflowIntegration,
TUpgradeProjectDTO TUpgradeProjectDTO
} from "./project-types"; } from "./project-types";
@@ -123,8 +129,19 @@ type TProjectServiceFactoryDep = {
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">; groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">; projectSlackConfigDAL: Pick<
TProjectSlackConfigDALFactory,
"findOne" | "transaction" | "updateById" | "create" | "delete"
>;
projectMicrosoftTeamsConfigDAL: Pick<
TProjectMicrosoftTeamsConfigDALFactory,
"findOne" | "transaction" | "updateById" | "create" | "delete"
>;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">; slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
microsoftTeamsIntegrationDAL: Pick<
TMicrosoftTeamsIntegrationDALFactory,
"findById" | "findByIdWithWorkflowIntegrationDetails"
>;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">; certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
@@ -197,7 +214,9 @@ export const projectServiceFactory = ({
kmsService, kmsService,
projectBotDAL, projectBotDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
projectMicrosoftTeamsConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
microsoftTeamsIntegrationDAL,
projectTemplateService, projectTemplateService,
groupProjectDAL, groupProjectDAL,
smtpService smtpService
@@ -1452,13 +1471,14 @@ export const projectServiceFactory = ({
return projectSshConfig; return projectSshConfig;
}; };
const getProjectSlackConfig = async ({ const getProjectWorkflowIntegrationConfig = async ({
actorId, actorId,
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
projectId projectId,
}: TGetProjectSlackConfig) => { integration
}: TGetProjectWorkflowIntegrationConfig) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) { if (!project) {
throw new NotFoundError({ throw new NotFoundError({
@@ -1477,23 +1497,60 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
return projectSlackConfigDAL.findOne({ if (integration === WorkflowIntegration.SLACK) {
projectId: project.id 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, actorId,
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
projectId, projectId,
slackIntegrationId, integration,
integrationId,
isAccessRequestNotificationEnabled, isAccessRequestNotificationEnabled,
accessRequestChannels, accessRequestChannels,
isSecretRequestNotificationEnabled, isSecretRequestNotificationEnabled,
secretRequestChannels secretRequestChannels
}: TUpdateProjectSlackConfig) => { }: TUpdateProjectWorkflowIntegration) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) { if (!project) {
throw new NotFoundError({ throw new NotFoundError({
@@ -1501,17 +1558,206 @@ export const projectServiceFactory = ({
}); });
} }
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId); if (integration === WorkflowIntegration.SLACK) {
const { permission } = await permissionService.getProjectPermission({
if (!slackIntegration) { actor,
throw new NotFoundError({ actorId,
message: `Slack integration with ID '${slackIntegrationId}' not found` 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 BadRequestError({
throw new ForbiddenRequestError({ message: `Integration type '${integration as string}' not supported`
message: "Selected slack integration is not in the same organization" });
};
const deleteProjectWorkflowIntegration = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
integrationId,
integration
}: TDeleteProjectWorkflowIntegration) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: `Project with ID '${projectId}' not found`
}); });
} }
@@ -1524,47 +1770,28 @@ export const projectServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
if (slackIntegration.orgId !== project.orgId) { if (integration === WorkflowIntegration.SLACK) {
throw new ForbiddenRequestError({ const [deletedIntegration] = await projectSlackConfigDAL.delete({
message: "Selected slack integration is not in the same organization" projectId,
slackIntegrationId: integrationId
}); });
return deletedIntegration;
} }
return projectSlackConfigDAL.transaction(async (tx) => { if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
const slackConfig = await projectSlackConfigDAL.findOne( const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({
{ projectId,
projectId microsoftTeamsIntegrationId: integrationId
}, });
tx
);
if (slackConfig) { return deletedIntegration;
return projectSlackConfigDAL.updateById( }
slackConfig.id,
{
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
}
return projectSlackConfigDAL.create( throw new BadRequestError({
{ message: `Integration with ID '${integrationId}' not found`
projectId,
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
}); });
}; };
@@ -1673,10 +1900,11 @@ export const projectServiceFactory = ({
getProjectKmsBackup, getProjectKmsBackup,
loadProjectKmsBackup, loadProjectKmsBackup,
getProjectKmsKeys, getProjectKmsKeys,
getProjectWorkflowIntegrationConfig,
updateProjectWorkflowIntegration,
deleteProjectWorkflowIntegration,
getProjectSshConfig, getProjectSshConfig,
updateProjectSshConfig, updateProjectSshConfig,
getProjectSlackConfig,
updateProjectSlackConfig,
requestProjectAccess, requestProjectAccess,
searchProjects searchProjects
}; };
+27 -7
View File
@@ -8,6 +8,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
enum KmsType { enum KmsType {
External = "external", External = "external",
@@ -166,14 +167,33 @@ export type TUpdateProjectSshConfig = {
export type TGetProjectSshConfig = TProjectPermission; export type TGetProjectSshConfig = TProjectPermission;
export type TGetProjectSlackConfig = TProjectPermission; export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & {
integration: WorkflowIntegration;
};
export type TUpdateProjectSlackConfig = { export type TUpdateProjectWorkflowIntegration = (
slackIntegrationId: string; | {
isAccessRequestNotificationEnabled: boolean; integrationId: string;
accessRequestChannels: string; integration: WorkflowIntegration.SLACK;
isSecretRequestNotificationEnabled: boolean; isAccessRequestNotificationEnabled: boolean;
secretRequestChannels: string; 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; } & TProjectPermission;
export type TBootstrapSshProjectDTO = { export type TBootstrapSshProjectDTO = {
+14 -43
View File
@@ -3,12 +3,10 @@ import { WebClient } from "@slack/web-api";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; 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 { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TSendSlackNotificationDTO } from "./slack-types";
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
export const fetchSlackChannels = async (botKey: string) => { export const fetchSlackChannels = async (botKey: string) => {
const slackChannels: { const slackChannels: {
@@ -41,11 +39,11 @@ export const fetchSlackChannels = async (botKey: string) => {
return slackChannels; return slackChannels;
}; };
const buildSlackPayload = (notification: TSlackNotification) => { const buildSlackPayload = (notification: TNotification) => {
const appCfg = getConfig(); const appCfg = getConfig();
switch (notification.type) { switch (notification.type) {
case SlackTriggerFeature.SECRET_APPROVAL: { case TriggerFeature.SECRET_APPROVAL: {
const { payload } = notification; const { payload } = notification;
const messageBody = `A secret approval request has been opened by ${payload.userEmail}. const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
*Environment*: ${payload.environment} *Environment*: ${payload.environment}
@@ -79,7 +77,7 @@ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId
payloadBlocks payloadBlocks
}; };
} }
case SlackTriggerFeature.ACCESS_REQUEST: { case TriggerFeature.ACCESS_REQUEST: {
const { payload } = notification; const { payload } = notification;
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${ const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
payload.isTemporary ? "temporary" : "permanent" payload.isTemporary ? "temporary" : "permanent"
@@ -125,51 +123,24 @@ User Note: ${payload.note}`
} }
}; };
export const triggerSlackNotification = async ({ export const sendSlackNotification = async ({
projectId, orgId,
notification, notification,
projectSlackConfigDAL, kmsService,
projectDAL, targetChannelIds,
kmsService slackIntegration
}: { }: TSendSlackNotificationDTO) => {
projectId: string;
notification: TSlackNotification;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
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;
}
}
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: project.orgId orgId
}); });
const botKey = orgDataKeyDecryptor({ const botKey = orgDataKeyDecryptor({
cipherTextBlob: slackIntegration.encryptedBotAccessToken cipherTextBlob: slackIntegration.encryptedBotAccessToken
}).toString("utf8"); }).toString("utf8");
const slackWebClient = new WebClient(botKey); const slackWebClient = new WebClient(botKey);
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
for await (const conversationId of targetChannelIds) { for await (const conversationId of targetChannelIds) {
// we send both text and blocks for compatibility with barebone clients // we send both text and blocks for compatibility with barebone clients
await slackWebClient.chat await slackWebClient.chat
+11 -31
View File
@@ -1,4 +1,8 @@
import { TSlackIntegrations } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { TNotification } from "@app/lib/workflow-integrations/types";
import { TKmsServiceFactory } from "../kms/kms-service";
export type TGetSlackInstallUrlDTO = { export type TGetSlackInstallUrlDTO = {
slug: string; slug: string;
@@ -48,34 +52,10 @@ export type TReinstallSlackIntegrationDTO = {
slackBotUserId: string; slackBotUserId: string;
}; };
export enum SlackTriggerFeature { export type TSendSlackNotificationDTO = {
SECRET_APPROVAL = "secret-approval", orgId: string;
ACCESS_REQUEST = "access-request" notification: TNotification;
} kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
targetChannelIds: string[];
export type TSlackNotification = slackIntegration: TSlackIntegrations;
| { };
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;
};
};
@@ -20,6 +20,7 @@ import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { RootKeyEncryptionStrategy } from "../kms/kms-types"; import { RootKeyEncryptionStrategy } from "../kms/kms-types";
import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
@@ -47,6 +48,7 @@ type TSuperAdminServiceFactoryDep = {
orgService: Pick<TOrgServiceFactory, "createOrganization">; orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">; keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
}; };
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>; export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
@@ -77,7 +79,8 @@ export const superAdminServiceFactory = ({
licenseService, licenseService,
identityAccessTokenDAL, identityAccessTokenDAL,
identityTokenAuthDAL, identityTokenAuthDAL,
identityOrgMembershipDAL identityOrgMembershipDAL,
microsoftTeamsService
}: TSuperAdminServiceFactoryDep) => { }: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => { const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself // TODO(akhilmhdh): bad pattern time less change this later to me itself
@@ -125,7 +128,13 @@ export const superAdminServiceFactory = ({
}; };
const updateServerCfg = async ( const updateServerCfg = async (
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string }, data: TSuperAdminUpdate & {
slackClientId?: string;
slackClientSecret?: string;
microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string;
},
userId: string userId: string
) => { ) => {
const updatedData = data; const updatedData = data;
@@ -192,10 +201,51 @@ export const superAdminServiceFactory = ({
updatedData.slackClientSecret = undefined; 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); const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); 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; return updatedServerCfg;
}; };
@@ -488,29 +538,40 @@ export const superAdminServiceFactory = ({
await userDAL.updateById(userId, { superAdmin: true }); await userDAL.updateById(userId, { superAdmin: true });
}; };
const getAdminSlackConfig = async () => { const getAdminIntegrationsConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg) { if (!serverCfg) {
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" }); throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
} }
let clientId = "";
let clientSecret = "";
const decrypt = kmsService.decryptWithRootKey(); const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) { const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
clientId = decrypt(serverCfg.encryptedSlackClientId).toString(); const slackClientSecret = serverCfg.encryptedSlackClientSecret
} ? decrypt(serverCfg.encryptedSlackClientSecret).toString()
: "";
if (serverCfg.encryptedSlackClientSecret) { const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString(); ? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
} : "";
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
: "";
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
: "";
return { return {
clientId, slack: {
clientSecret clientSecret: slackClientSecret,
clientId: slackClientId
},
microsoftTeams: {
appId: microsoftAppId,
clientSecret: microsoftClientSecret,
botId: microsoftBotId
}
}; };
}; };
@@ -578,7 +639,7 @@ export const superAdminServiceFactory = ({
getUsers, getUsers,
deleteUser, deleteUser,
getIdentities, getIdentities,
getAdminSlackConfig, getAdminIntegrationsConfig,
updateRootEncryptionStrategy, updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies, getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser, grantServerAdminAccessToUser,
@@ -1,7 +1,13 @@
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
export enum WorkflowIntegration { export enum WorkflowIntegration {
SLACK = "slack" SLACK = "slack",
MICROSOFT_TEAMS = "microsoft-teams"
}
export enum WorkflowIntegrationStatus {
PENDING = "pending",
INSTALLED = "installed"
} }
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">; export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
@@ -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
<Tabs>
<Tab title="Infisical Cloud">
<MicrosoftTeamsWorkflowIntegration />
</Tab>
<Tab title="Self-hosted setup">
### 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.
<Steps>
<Step title="Create Microsoft Teams app">
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)
</Step>
<Step title="Create a Microsoft Teams bot">
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)
</Step>
<Step title="Add a message endpoint to the bot">
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://<your-infisical-instance-url>/api/v1/workflow-integrations/microsoft-teams/message-endpoint`
Replace `<your-infisical-instance-url>` 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)
</Step>
<Step title="Find the bot in Azure App Registrations">
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)
</Step>
<Step title="Add API Permissions to the App Registration">
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)
</Step>
<Step title="Add a new web application to the App Registration">
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://<your-infisical-instance-url>/organization/settings/oauth/callback`. Replace `<your-infisical-instance-url>` 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)
</Step>
<Step title="Get App Registration Client ID and Client Secret">
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)
<Warning>
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.
</Warning>
</Step>
<Step title="Get the Microsoft Teams App ID">
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)
</Step>
<Step title="Link the Microsoft Teams App to the bot">
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)
</Step>
<Step title="Run an app validation test (Recommended)">
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)
<Note>
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.
</Note>
![microsoft-teams-app-validation-test-results](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation-result.png)
<Note>
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.
</Note>
</Step>
<Step title="Download the App Package">
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)
</Step>
</Steps>
### 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.
<Steps>
<Step title="Navigate to the Integrations tab in your Server Admin Console 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.
</Step>
</Steps>
<MicrosoftTeamsWorkflowIntegration />
</Tab>
</Tabs>
## Troubleshooting
<Accordion title="Notifications are not being sent to Microsoft Teams even though the integration is configured">
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.
</Accordion>
<Accordion title="Workflow Integration is stuck on 'Pending' status">
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)
</Accordion>
@@ -1,6 +1,6 @@
--- ---
title: "Slack integration" title: "Slack Integration"
description: "Learn how to setup 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. This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

+2 -1
View File
@@ -222,7 +222,8 @@
{ {
"group": "Workflow Integrations", "group": "Workflow Integrations",
"pages": [ "pages": [
"documentation/platform/workflow-integrations/slack-integration" "documentation/platform/workflow-integrations/slack-integration",
"documentation/platform/workflow-integrations/microsoft-teams-integration"
] ]
}, },
{ {
@@ -0,0 +1,100 @@
### Create Microsoft Teams workflow integration
<Steps>
<Step title="Install the Infisical Microsoft Teams App in Microsoft Teams tenant">
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)
<Note>
**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.
</Note>
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).
<Warning>
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.
</Warning>
Once the app has been approved, you will be able to use the Infisical Microsoft Teams integration in your projects.
</Step>
<Step title="Add the Infisical Microsoft Teams app to your Microsoft Teams teams">
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.
<Info>
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.
</Info>
Once the app has been added to the team, you will be able to use the Infisical Microsoft Teams integration in the team.
</Step>
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
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)
</Step>
<Step title="Create a Microsoft Teams workflow integration">
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)
<Note>
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.
</Note>
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)
</Step>
</Steps>
### Configure project to use Microsoft Teams workflow integration
<Steps>
<Step title="Navigate to the Workflow Integrations tab in the project settings">
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.
</Step>
<Step title="Configure the Microsoft Teams workflow integration">
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)
</Step>
</Steps>
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.
@@ -47,8 +47,10 @@ AccordionTrigger.displayName = "AccordionTrigger";
export const AccordionContent = forwardRef< export const AccordionContent = forwardRef<
HTMLDivElement, HTMLDivElement,
AccordionPrimitive.AccordionContentProps AccordionPrimitive.AccordionContentProps & {
>(({ children, className, ...props }, forwardedRef) => ( childrenClassName?: string;
}
>(({ children, className, childrenClassName, ...props }, forwardedRef) => (
<AccordionPrimitive.Content <AccordionPrimitive.Content
className={twMerge( className={twMerge(
"overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown", "overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown",
@@ -57,7 +59,7 @@ export const AccordionContent = forwardRef<
{...props} {...props}
ref={forwardedRef} ref={forwardedRef}
> >
<div className="px-4 py-2 text-sm">{children}</div> <div className={twMerge("px-4 py-2 text-sm", childrenClassName)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
)); ));
+7 -1
View File
@@ -17,6 +17,12 @@ export const ROUTE_PATHS = Object.freeze({
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup") PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
}, },
Organization: { Organization: {
Settings: {
OauthCallbackPage: setRoute(
"/organization/settings/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
)
},
SecretScanning: setRoute( SecretScanning: setRoute(
"/organization/secret-scanning", "/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning" "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
@@ -31,7 +37,7 @@ export const ROUTE_PATHS = Object.freeze({
), ),
SettingsPage: setRoute( SettingsPage: setRoute(
"/organization/settings", "/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/settings" "/_authenticate/_inject-org-details/_org-layout/organization/settings/"
), ),
GroupDetailsByIDPage: setRoute( GroupDetailsByIDPage: setRoute(
"/organization/groups/$groupId", "/organization/groups/$groupId",
+1 -2
View File
@@ -4,13 +4,12 @@ export {
useAdminRemoveIdentitySuperAdminAccess, useAdminRemoveIdentitySuperAdminAccess,
useCreateAdminUser, useCreateAdminUser,
useRemoveUserServerAdminAccess, useRemoveUserServerAdminAccess,
useUpdateAdminSlackConfig,
useUpdateServerConfig, useUpdateServerConfig,
useUpdateServerEncryptionStrategy useUpdateServerEncryptionStrategy
} from "./mutation"; } from "./mutation";
export { export {
useAdminGetUsers, useAdminGetUsers,
useGetAdminSlackConfig, useGetAdminIntegrationsConfig,
useGetServerConfig, useGetServerConfig,
useGetServerRootKmsEncryptionDetails useGetServerRootKmsEncryptionDetails
} from "./queries"; } from "./queries";
+3 -24
View File
@@ -6,11 +6,10 @@ import { organizationKeys } from "../organization/queries";
import { User } from "../users/types"; import { User } from "../users/types";
import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import { import {
AdminSlackConfig,
RootKeyEncryptionStrategy, RootKeyEncryptionStrategy,
TCreateAdminUserDTO, TCreateAdminUserDTO,
TServerConfig, TServerConfig,
TUpdateAdminSlackConfigDTO TUpdateServerConfigDTO
} from "./types"; } from "./types";
export const useCreateAdminUser = () => { export const useCreateAdminUser = () => {
@@ -34,11 +33,7 @@ export const useCreateAdminUser = () => {
export const useUpdateServerConfig = () => { export const useUpdateServerConfig = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation< return useMutation<TServerConfig, object, TUpdateServerConfigDTO>({
TServerConfig,
object,
Partial<TServerConfig & { slackClientId: string; slackClientSecret: string }>
>({
mutationFn: async (opt) => { mutationFn: async (opt) => {
const { data } = await apiRequest.patch<{ config: TServerConfig }>( const { data } = await apiRequest.patch<{ config: TServerConfig }>(
"/api/v1/admin/config", "/api/v1/admin/config",
@@ -48,6 +43,7 @@ export const useUpdateServerConfig = () => {
}, },
onSuccess: (data) => { onSuccess: (data) => {
queryClient.setQueryData(adminQueryKeys.serverConfig(), data); queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminIntegrationsConfig() });
queryClient.invalidateQueries({ queryKey: adminQueryKeys.serverConfig() }); queryClient.invalidateQueries({ queryKey: adminQueryKeys.serverConfig() });
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations }); queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
} }
@@ -119,23 +115,6 @@ export const useAdminGrantServerAdminAccess = () => {
}); });
}; };
export const useUpdateAdminSlackConfig = () => {
const queryClient = useQueryClient();
return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.put<AdminSlackConfig>(
"/api/v1/admin/integrations/slack/config",
dto
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminSlackConfig() });
}
});
};
export const useUpdateServerEncryptionStrategy = () => { export const useUpdateServerEncryptionStrategy = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
+5 -7
View File
@@ -7,7 +7,7 @@ import { User } from "../types";
import { import {
AdminGetIdentitiesFilters, AdminGetIdentitiesFilters,
AdminGetUsersFilters, AdminGetUsersFilters,
AdminSlackConfig, AdminIntegrationsConfig,
TGetServerRootKmsEncryptionDetails, TGetServerRootKmsEncryptionDetails,
TServerConfig TServerConfig
} from "./types"; } from "./types";
@@ -22,7 +22,7 @@ export const adminQueryKeys = {
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getIdentities: (filters: AdminGetIdentitiesFilters) => getIdentities: (filters: AdminGetIdentitiesFilters) =>
[adminStandaloneKeys.getIdentities, { filters }] as const, [adminStandaloneKeys.getIdentities, { filters }] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const, getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] 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({ return useQuery({
queryKey: adminQueryKeys.getAdminSlackConfig(), queryKey: adminQueryKeys.getAdminIntegrationsConfig(),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<AdminSlackConfig>( const { data } = await apiRequest.get<AdminIntegrationsConfig>("/api/v1/admin/integrations");
"/api/v1/admin/integrations/slack/config"
);
return data; return data;
} }
+18 -8
View File
@@ -26,6 +26,14 @@ export type TServerConfig = {
pageFrameContent?: string; pageFrameContent?: string;
}; };
export type TUpdateServerConfigDTO = {
slackClientId?: string;
slackClientSecret?: string;
microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string;
} & Partial<TServerConfig>;
export type TCreateAdminUserDTO = { export type TCreateAdminUserDTO = {
email: string; email: string;
password: string; password: string;
@@ -42,11 +50,6 @@ export type TCreateAdminUserDTO = {
salt: string; salt: string;
}; };
export type TUpdateAdminSlackConfigDTO = {
clientId: string;
clientSecret: string;
};
export type AdminGetUsersFilters = { export type AdminGetUsersFilters = {
limit: number; limit: number;
searchTerm: string; searchTerm: string;
@@ -58,9 +61,16 @@ export type AdminGetIdentitiesFilters = {
searchTerm: string; searchTerm: string;
}; };
export type AdminSlackConfig = { export type AdminIntegrationsConfig = {
clientId: string; slack: {
clientSecret: string; clientId: string;
clientSecret: string;
};
microsoftTeams: {
appId: string;
clientSecret: string;
botId: string;
};
}; };
export type TGetServerRootKmsEncryptionDetails = { export type TGetServerRootKmsEncryptionDetails = {
+20 -3
View File
@@ -94,8 +94,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
"Create certificate template EST configuration", "Create certificate template EST configuration",
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]: [EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
"Update certificate template EST configuration", "Update certificate template EST configuration",
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration", [EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration", "Update project workflow integration configuration",
[EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
"Get project workflow integration configuration",
[EventType.INTEGRATION_SYNCED]: "Integration sync", [EventType.INTEGRATION_SYNCED]: "Integration sync",
[EventType.CREATE_SHARED_SECRET]: "Create shared secret", [EventType.CREATE_SHARED_SECRET]: "Create shared secret",
[EventType.DELETE_SHARED_SECRET]: "Delete 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.CREATE_SECRET_ROTATION]: "Create Secret Rotation",
[EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation", [EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation",
[EventType.DELETE_SECRET_ROTATION]: "Delete 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 } = { export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+14 -3
View File
@@ -98,8 +98,8 @@ export enum EventType {
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config", CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
INTEGRATION_SYNCED = "integration-synced", INTEGRATION_SYNCED = "integration-synced",
CREATE_SHARED_SECRET = "create-shared-secret", CREATE_SHARED_SECRET = "create-shared-secret",
DELETE_SHARED_SECRET = "delete-shared-secret", DELETE_SHARED_SECRET = "delete-shared-secret",
@@ -163,5 +163,16 @@ export enum EventType {
CREATE_SECRET_ROTATION = "create-secret-rotation", CREATE_SECRET_ROTATION = "create-secret-rotation",
UPDATE_SECRET_ROTATION = "update-secret-rotation", UPDATE_SECRET_ROTATION = "update-secret-rotation",
DELETE_SECRET_ROTATION = "delete-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"
} }
+10 -7
View File
@@ -1,6 +1,7 @@
import { CaStatus } from "../ca"; import { CaStatus } from "../ca";
import { IdentityTrustedIp } from "../identities/types"; import { IdentityTrustedIp } from "../identities/types";
import { PkiItemType } from "../pkiCollections/constants"; import { PkiItemType } from "../pkiCollections/constants";
import { WorkflowIntegration } from "../workflowIntegrations/types";
import { ActorType, EventType, UserAgentType } from "./enums"; import { ActorType, EventType, UserAgentType } from "./enums";
export type TGetAuditLogsFilter = { export type TGetAuditLogsFilter = {
@@ -786,11 +787,12 @@ interface GetCertificateTemplateEstConfig {
}; };
} }
interface UpdateProjectSlackConfig { interface UpdateProjectWorkflowIntegrationConfig {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG; type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: { metadata: {
id: string; id: string;
slackIntegrationId: string; integrationId: string;
integration: WorkflowIntegration;
isAccessRequestNotificationEnabled: boolean; isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string; accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean; isSecretRequestNotificationEnabled: boolean;
@@ -798,10 +800,11 @@ interface UpdateProjectSlackConfig {
}; };
} }
interface GetProjectSlackConfig { interface GetProjectWorkflowIntegrationConfig {
type: EventType.GET_PROJECT_SLACK_CONFIG; type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: { metadata: {
id: string; id: string;
integration: WorkflowIntegration;
}; };
} }
@@ -898,8 +901,8 @@ export type Event =
| UpdateCertificateTemplateEstConfig | UpdateCertificateTemplateEstConfig
| CreateCertificateTemplateEstConfig | CreateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig | GetCertificateTemplateEstConfig
| UpdateProjectSlackConfig | UpdateProjectWorkflowIntegrationConfig
| GetProjectSlackConfig | GetProjectWorkflowIntegrationConfig
| IntegrationSyncedEvent; | IntegrationSyncedEvent;
export type AuditLog = { export type AuditLog = {
@@ -1,13 +1,2 @@
export { export * from "./mutation";
useDeleteSlackIntegration, export * from "./queries";
useUpdateProjectSlackConfig,
useUpdateSlackIntegration
} from "./mutation";
export {
fetchSlackInstallUrl,
fetchSlackReinstallUrl,
useGetSlackIntegrationById,
useGetSlackIntegrationChannels,
useGetSlackIntegrations,
useGetWorkflowIntegrations
} from "./queries";
@@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/query-keys"; import { workspaceKeys } from "../workspace/query-keys";
import { workflowIntegrationKeys } from "./queries"; import { workflowIntegrationKeys } from "./queries";
import { import {
TCheckMicrosoftTeamsIntegrationInstallationStatusDTO,
TCreateMicrosoftTeamsIntegrationDTO,
TDeleteMicrosoftTeamsIntegrationDTO,
TDeleteProjectWorkflowIntegrationDTO,
TDeleteSlackIntegrationDTO, TDeleteSlackIntegrationDTO,
TUpdateProjectSlackConfigDTO, TUpdateMicrosoftTeamsIntegrationDTO,
TUpdateProjectWorkflowIntegrationConfigDTO,
TUpdateSlackIntegrationDTO TUpdateSlackIntegrationDTO
} from "./types"; } from "./types";
@@ -28,6 +33,47 @@ export const useUpdateSlackIntegration = () => {
}); });
}; };
export const useUpdateMicrosoftTeamsIntegration = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TUpdateMicrosoftTeamsIntegrationDTO>({
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<object, object, TCreateMicrosoftTeamsIntegrationDTO>({
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 = () => { export const useDeleteSlackIntegration = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -44,21 +90,86 @@ export const useDeleteSlackIntegration = () => {
}); });
}; };
export const useUpdateProjectSlackConfig = () => { export const useDeleteMicrosoftTeamsIntegration = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TDeleteMicrosoftTeamsIntegrationDTO>({
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(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (dto: TUpdateProjectSlackConfigDTO) => { mutationFn: async (dto: TUpdateProjectWorkflowIntegrationConfigDTO) => {
const { data } = await apiRequest.put( const { data } = await apiRequest.put(
`/api/v1/workspace/${dto.workspaceId}/slack-config`, `/api/v1/workspace/${dto.workspaceId}/workflow-integration`,
dto dto
); );
return data; return data;
}, },
onSuccess: (_, { workspaceId }) => { onSuccess: (_, { workspaceId, integration }) => {
queryClient.invalidateQueries({ 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) });
}
});
};
@@ -2,13 +2,29 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { SlackIntegration, SlackIntegrationChannel, WorkflowIntegration } from "./types"; import {
MicrosoftTeamsIntegration,
MicrosoftTeamsIntegrationTeam,
SlackIntegration,
SlackIntegrationChannel,
WorkflowIntegration
} from "./types";
export const workflowIntegrationKeys = { export const workflowIntegrationKeys = {
getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"], getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"],
getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"], getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"], 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 ({ export const fetchSlackInstallUrl = async ({
@@ -66,13 +82,58 @@ export const fetchWorkflowIntegrations = async () => {
return data; return data;
}; };
export const useGetSlackIntegrations = (orgId?: string) => export const fetchMicrosoftTeamsIntegrations = async () => {
const { data } = await apiRequest.get<MicrosoftTeamsIntegration[]>(
"/api/v1/workflow-integrations/microsoft-teams"
);
return data;
};
export const fetchMicrosoftTeamsIntegrationById = async (id?: string) => {
const { data } = await apiRequest.get<MicrosoftTeamsIntegration>(
`/api/v1/workflow-integrations/microsoft-teams/${id}`
);
return data;
};
export const fetchMicrosoftTeamsIntegrationTeams = async (id?: string) => {
const { data } = await apiRequest.get<MicrosoftTeamsIntegrationTeam[]>(
`/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({ useQuery({
queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId), queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId),
queryFn: () => fetchSlackIntegrations(), queryFn: () => fetchMicrosoftTeamsIntegrations(),
enabled: Boolean(orgId) 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) => export const useGetSlackIntegrationById = (id?: string) =>
useQuery({ useQuery({
queryKey: workflowIntegrationKeys.getSlackIntegration(id), queryKey: workflowIntegrationKeys.getSlackIntegration(id),
@@ -93,3 +154,10 @@ export const useGetWorkflowIntegrations = (id?: string) =>
queryFn: () => fetchWorkflowIntegrations(), queryFn: () => fetchWorkflowIntegrations(),
enabled: Boolean(id) enabled: Boolean(id)
}); });
export const useGetMicrosoftTeamsClientId = () =>
useQuery({
queryKey: workflowIntegrationKeys.getMicrosoftTeamsClientId(),
queryFn: () => fetchMicrosoftTeamsClientId(),
enabled: true
});
@@ -1,14 +1,25 @@
export enum WorkflowIntegrationPlatform { export enum WorkflowIntegrationPlatform {
SLACK = "slack" SLACK = "slack",
MICROSOFT_TEAMS = "microsoft-teams"
} }
export type WorkflowIntegration = { export type WorkflowIntegration = {
id: string; id: string;
slug: string; slug: string;
description: string; description: string;
status: WorkflowIntegrationStatus;
integration: WorkflowIntegrationPlatform; integration: WorkflowIntegrationPlatform;
}; };
export type MicrosoftTeamsIntegrationTeam = {
teamId: string;
teamName: string;
channels: {
channelId: string;
channelName: string;
}[];
};
export type SlackIntegration = { export type SlackIntegration = {
id: string; id: string;
slug: string; slug: string;
@@ -16,6 +27,18 @@ export type SlackIntegration = {
teamName: string; teamName: string;
}; };
export enum WorkflowIntegrationStatus {
Pending = "pending",
Installed = "installed"
}
export type MicrosoftTeamsIntegration = {
id: string;
slug: string;
description: string;
tenantId: string;
};
export type SlackIntegrationChannel = { export type SlackIntegrationChannel = {
id: string; id: string;
name: string; name: string;
@@ -28,25 +51,90 @@ export type TUpdateSlackIntegrationDTO = {
description?: string; 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 = { export type TDeleteSlackIntegrationDTO = {
id: string; id: string;
orgId: string; orgId: string;
}; };
export type ProjectSlackConfig = { export type TDeleteMicrosoftTeamsIntegrationDTO = {
id: string; id: string;
slackIntegrationId: string; orgId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
}; };
export type TUpdateProjectSlackConfigDTO = { export type ProjectWorkflowIntegrationConfig =
workspaceId: string; | {
slackIntegrationId: string; id: string;
isAccessRequestNotificationEnabled: boolean; integration: WorkflowIntegrationPlatform.SLACK;
accessRequestChannels: string; integrationId: string;
isSecretRequestNotificationEnabled: boolean; isAccessRequestNotificationEnabled: boolean;
secretRequestChannels: string; 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;
}; };
+1 -1
View File
@@ -26,9 +26,9 @@ export {
useGetWorkspaceIndexStatus, useGetWorkspaceIndexStatus,
useGetWorkspaceIntegrations, useGetWorkspaceIntegrations,
useGetWorkspaceSecrets, useGetWorkspaceSecrets,
useGetWorkspaceSlackConfig,
useGetWorkspaceUserDetails, useGetWorkspaceUserDetails,
useGetWorkspaceUsers, useGetWorkspaceUsers,
useGetWorkspaceWorkflowIntegrationConfig,
useListWorkspaceCas, useListWorkspaceCas,
useListWorkspaceCertificates, useListWorkspaceCertificates,
useListWorkspaceCertificateTemplates, useListWorkspaceCertificateTemplates,
+23 -6
View File
@@ -20,7 +20,10 @@ import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
import { TSshHost } from "../sshHost/types"; import { TSshHost } from "../sshHost/types";
import { userKeys } from "../users/query-keys"; import { userKeys } from "../users/query-keys";
import { TWorkspaceUser } from "../users/types"; import { TWorkspaceUser } from "../users/types";
import { ProjectSlackConfig } from "../workflowIntegrations/types"; import {
ProjectWorkflowIntegrationConfig,
WorkflowIntegrationPlatform
} from "../workflowIntegrations/types";
import { workspaceKeys } from "./query-keys"; import { workspaceKeys } from "./query-keys";
import { import {
CreateEnvironmentDTO, CreateEnvironmentDTO,
@@ -883,13 +886,27 @@ export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
}); });
}; };
export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => { export const useGetWorkspaceWorkflowIntegrationConfig = ({
workspaceId,
integration
}: {
workspaceId: string;
integration: WorkflowIntegrationPlatform;
}) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId), queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<ProjectSlackConfig>( const { data } = await apiRequest
`/api/v1/workspace/${workspaceId}/slack-config` .get<ProjectWorkflowIntegrationConfig>(
); `/api/v1/workspace/${workspaceId}/workflow-integration-config/${integration}`
)
.catch((err) => {
if (err.response.status === 404) {
return { data: null };
}
throw err;
});
return data; return data;
}, },
@@ -1,6 +1,7 @@
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types"; import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types";
import type { CaStatus } from "../ca"; import type { CaStatus } from "../ca";
import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types";
export const workspaceKeys = { export const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const, getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const,
@@ -54,8 +55,10 @@ export const workspaceKeys = {
[{ workspaceId }, "workspace-pki-collections"] as const, [{ workspaceId }, "workspace-pki-collections"] as const,
getWorkspaceCertificateTemplates: (workspaceId: string) => getWorkspaceCertificateTemplates: (workspaceId: string) =>
[{ workspaceId }, "workspace-certificate-templates"] as const, [{ workspaceId }, "workspace-certificate-templates"] as const,
getWorkspaceSlackConfig: (workspaceId: string) => getWorkspaceWorkflowIntegrationConfig: (
[{ workspaceId }, "workspace-slack-config"] as const, workspaceId: string,
integration: WorkflowIntegrationPlatform
) => [{ workspaceId, integration }, "workspace-workflow-integration-config"] as const,
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const, getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
allWorkspaceSshCertificates: (projectId: string) => allWorkspaceSshCertificates: (projectId: string) =>
[{ projectId }, "workspace-ssh-certificates"] as const, [{ projectId }, "workspace-ssh-certificates"] as const,
+1 -1
View File
@@ -197,4 +197,4 @@ html {
position: absolute; position: absolute;
top: 0.5rem; top: 0.5rem;
@apply text-sm text-gray-500 opacity-50; @apply text-sm text-gray-500 opacity-50;
} }
@@ -1,166 +1,24 @@
import { useEffect } from "react"; import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { Button, FormControl, Input } from "@app/components/v2"; import { SlackIntegrationForm } from "./SlackIntegrationForm";
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<typeof slackFormSchema>;
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
}
})
)}`;
export const IntegrationPanel = () => { export const IntegrationPanel = () => {
const { const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
control,
handleSubmit,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TSlackForm>({
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"
});
};
return ( return (
<form <div className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" <div className="mb-4">
onSubmit={handleSubmit(onSlackFormSubmit)} <div className="text-xl font-semibold text-mineshaft-100">Integrations</div>
> <div className="text-sm text-mineshaft-300">
<div className="flex flex-col justify-start"> Configure your instance-wide settings to enable integration with Slack and Microsoft
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Slack Integration</div> Teams.
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 1: Create your Infisical Slack App
</div> </div>
<div className="mb-6">
<Button
colorSchema="secondary"
onClick={() => window.open(getCustomSlackAppCreationUrl())}
>
Create Slack App
</Button>
</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
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.
</div>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client ID"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientIdFocused ? "text" : "password"}
onFocus={() => setIsSlackClientIdFocused.on()}
onBlur={() => setIsSlackClientIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientSecretFocused ? "text" : "password"}
onFocus={() => setIsSlackClientSecretFocused.on()}
onBlur={() => setIsSlackClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div> </div>
<Button <div className="flex flex-col gap-2">
className="mt-2" <SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
type="submit" <MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
isLoading={isSubmitting} </div>
isDisabled={isSubmitting || !isDirty} </div>
>
Save
</Button>
</form>
); );
}; };
@@ -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<typeof microsoftTeamsFormSchema>;
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<TMicrosoftTeamsForm>({
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 (
<form onSubmit={handleSubmit(onSubmit)}>
<Accordion type="single" collapsible className="w-full">
<AccordionItem
value="microsoft-teams-integration"
className="data-[state=open]:border-none"
>
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
<div className="text-md group order-1 ml-3 flex items-center gap-2">
<BsMicrosoftTeams className="text-lg group-hover:text-primary-400" />
<div className="text-[15px] font-semibold">Microsoft Teams Integration</div>
</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
<div className="mb-2 max-w-lg text-sm text-mineshaft-300">
Step 1: Create and configure Microsoft Teams bot and Azure Resources. Please refer
to the documentation below for more information.
</div>
<div className="mb-6">
<a
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
target="_blank"
rel="noopener noreferrer"
>
<Button colorSchema="secondary">Documentation</Button>
</a>
</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
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&apos;s App
Registration page. The Client Secret is the password for the bot.
</div>
<Controller
control={control}
name="appId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Application (Client) ID"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isMicrosoftTeamsAppIdFocused ? "text" : "password"}
onFocus={() => setIsMicrosoftTeamsAppIdFocused.on()}
onBlur={() => setIsMicrosoftTeamsAppIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
tooltipText="You can find your Client Secret in the Certificates & Secrets section of your Microsoft Teams bot's App Registration."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isMicrosoftTeamsClientSecretFocused ? "text" : "password"}
onFocus={() => setIsMicrosoftTeamsClientSecretFocused.on()}
onBlur={() => setIsMicrosoftTeamsClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="botId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams App ID"
tooltipText="You can find the Microsoft Teams App ID in the overview of your Microsoft Teams App inside the Microsoft Teams Developer Portal."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isMicrosoftTeamsBotIdFocused ? "text" : "password"}
onFocus={() => setIsMicrosoftTeamsBotIdFocused.on()}
onBlur={() => setIsMicrosoftTeamsBotIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</form>
);
};
@@ -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<typeof slackFormSchema>;
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<TSlackForm>({
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 (
<form onSubmit={handleSubmit(onSubmit)}>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="slack-integration" className="data-[state=open]:border-none">
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
<div className="text-md group order-1 ml-3 flex items-center gap-2">
<BsSlack className="text-lg group-hover:text-primary-400" />
<div className="text-[15px] font-semibold">Slack Integration</div>
</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 1: Create your Infisical Slack App
</div>
<div className="mb-6">
<Button
colorSchema="secondary"
onClick={() => window.open(getCustomSlackAppCreationUrl())}
>
Create Slack App
</Button>
</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
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.
</div>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client ID"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientIdFocused ? "text" : "password"}
onFocus={() => setIsSlackClientIdFocused.on()}
onBlur={() => setIsSlackClientIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isSlackClientSecretFocused ? "text" : "password"}
onFocus={() => setIsSlackClientSecretFocused.on()}
onBlur={() => setIsSlackClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</form>
);
};
@@ -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 (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader text="Please wait! Authentication in process." />
</div>
);
};
@@ -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)
});
@@ -1,4 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { BsMicrosoftTeams } from "react-icons/bs";
import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
@@ -6,6 +7,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2"; import { Modal, ModalContent } from "@app/components/v2";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm"; import { SlackIntegrationForm } from "./SlackIntegrationForm";
type Props = { type Props = {
@@ -20,9 +22,14 @@ enum WizardSteps {
const PLATFORM_LIST = [ const PLATFORM_LIST = [
{ {
icon: faSlack, icon: <FontAwesomeIcon icon={faSlack} size="lg" />,
platform: WorkflowIntegrationPlatform.SLACK, platform: WorkflowIntegrationPlatform.SLACK,
title: "Slack" title: "Slack"
},
{
icon: <BsMicrosoftTeams className="text-lg" />,
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
title: "Microsoft Teams"
} }
]; ];
@@ -38,7 +45,10 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
return ( return (
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}> <Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
<ModalContent title={`Add a ${selectedPlatform ?? "workflow"} integration`} className="my-4"> <ModalContent
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
className="my-4"
>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{wizardStep === WizardSteps.SelectPlatform && ( {wizardStep === WizardSteps.SelectPlatform && (
<motion.div <motion.div
@@ -67,7 +77,7 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
} }
}} }}
> >
<FontAwesomeIcon icon={icon} size="lg" /> <div>{icon}</div>
<div className="whitespace-pre-wrap text-center text-sm">{title}</div> <div className="whitespace-pre-wrap text-center text-sm">{title}</div>
</div> </div>
))} ))}
@@ -86,6 +96,18 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
<SlackIntegrationForm onClose={() => onToggle(false)} /> <SlackIntegrationForm onClose={() => onToggle(false)} />
</motion.div> </motion.div>
)} )}
{wizardStep === WizardSteps.PlatformInputs &&
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<motion.div
key="microsoft-teams-platform"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<MicrosoftTeamsIntegrationForm onClose={() => onToggle(false)} />
</motion.div>
)}
</AnimatePresence> </AnimatePresence>
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -1,6 +1,7 @@
import { Modal, ModalContent } from "@app/components/v2"; import { Modal, ModalContent } from "@app/components/v2";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm"; import { SlackIntegrationForm } from "./SlackIntegrationForm";
type Props = { type Props = {
@@ -20,6 +21,9 @@ export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatf
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && ( {workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} /> <SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
)} )}
{workflowPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<MicrosoftTeamsIntegrationForm id={id} onClose={() => onOpenChange(false)} />
)}
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
@@ -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<typeof microsoftTeamsFormSchema>;
export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
const {
control,
handleSubmit,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TMicrosoftTeamsFormData>({
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 (
<form onSubmit={handleSubmit(handleMicrosoftTeamsFormSubmit)} autoComplete="off">
<div className="mb-4 text-xs text-mineshaft-200">
For seamless installations, ensure that the Infisical bot is already installed in your
Microsoft Teams tenant. For more information, please refer to the{" "}
<a
className="text-primary-500"
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
target="_blank"
rel="noopener noreferrer"
>
Microsoft Teams Workflow Integration Documentation
</a>
, which will guide you through the download and installation process.
</div>
<Controller
control={control}
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" isRequired errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
{!microsoftTeamsIntegration && (
<Controller
control={control}
name="tenantId"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Tenant ID"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
)}
<Controller
control={control}
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
{microsoftTeamsIntegration && (
<FormControl label="Connected Microsoft Teams Tenant">
<Input
value={microsoftTeamsIntegration.tenantId}
isReadOnly
className="bg-white/[0.07]"
/>
</FormControl>
)}
<div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={!isDirty || isSubmitting}>
{id && microsoftTeamsIntegration ? "Save" : "Create Microsoft Teams Integration"}
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
);
};
@@ -1,5 +1,5 @@
import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faGear, faInfoCircle, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios"; import axios from "axios";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Badge,
Button, Button,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
@@ -20,6 +21,7 @@ import {
TBody, TBody,
Td, Td,
THead, THead,
Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
@@ -27,14 +29,31 @@ import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
fetchSlackReinstallUrl, fetchSlackReinstallUrl,
useCheckMicrosoftTeamsIntegrationInstallationStatus,
useDeleteMicrosoftTeamsIntegration,
useDeleteSlackIntegration, useDeleteSlackIntegration,
useGetWorkflowIntegrations useGetWorkflowIntegrations
} from "@app/hooks/api"; } 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 { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
import { IntegrationFormDetails } from "./IntegrationFormDetails"; import { IntegrationFormDetails } from "./IntegrationFormDetails";
const renderStatus = (status: WorkflowIntegrationStatus) => {
if (status === WorkflowIntegrationStatus.Installed) {
return <Badge variant="success">Installed</Badge>;
}
if (status === WorkflowIntegrationStatus.Pending) {
return <Badge variant="primary">Pending</Badge>;
}
return <Badge variant="danger">Failed</Badge>;
};
export const OrgWorkflowIntegrationTab = withPermission( export const OrgWorkflowIntegrationTab = withPermission(
() => { () => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
@@ -48,6 +67,9 @@ export const OrgWorkflowIntegrationTab = withPermission(
useGetWorkflowIntegrations(currentOrg?.id); useGetWorkflowIntegrations(currentOrg?.id);
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
const { mutateAsync: deleteMicrosoftTeamsIntegration } = useDeleteMicrosoftTeamsIntegration();
const { mutateAsync: checkMicrosoftTeamsInstallationStatus } =
useCheckMicrosoftTeamsIntegrationInstallationStatus();
const handleRemoveIntegration = async () => { const handleRemoveIntegration = async () => {
if (!currentOrg) { if (!currentOrg) {
@@ -62,6 +84,13 @@ export const OrgWorkflowIntegrationTab = withPermission(
}); });
} }
if (platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
await deleteMicrosoftTeamsIntegration({
id,
orgId: currentOrg?.id
});
}
handlePopUpClose("removeIntegration"); handlePopUpClose("removeIntegration");
createNotification({ createNotification({
text: "Successfully deleted integration", text: "Successfully deleted integration",
@@ -69,27 +98,37 @@ export const OrgWorkflowIntegrationTab = withPermission(
}); });
}; };
const triggerReinstall = async (platform: WorkflowIntegrationPlatform, id: string) => { const triggerSlackReinstall = async (id: string) => {
if (platform === WorkflowIntegrationPlatform.SLACK) { try {
try { const slackReinstallUrl = await fetchSlackReinstallUrl({
const slackReinstallUrl = await fetchSlackReinstallUrl({ id
id });
});
if (slackReinstallUrl) { if (slackReinstallUrl) {
window.location.href = slackReinstallUrl; window.location.href = slackReinstallUrl;
} }
} catch (err) { } catch (err) {
if (axios.isAxiosError(err)) { if (axios.isAxiosError(err)) {
createNotification({ createNotification({
text: (err.response?.data as { message: string })?.message, text: (err.response?.data as { message: string })?.message,
type: "error" 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 ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between"> <div className="flex justify-between">
@@ -117,6 +156,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
<Tr> <Tr>
<Td>Provider</Td> <Td>Provider</Td>
<Td>Alias</Td> <Td>Alias</Td>
<Td>Status</Td>
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
@@ -134,11 +174,23 @@ export const OrgWorkflowIntegrationTab = withPermission(
)} )}
{workflowIntegrations?.map((workflowIntegration) => ( {workflowIntegrations?.map((workflowIntegration) => (
<Tr key={workflowIntegration.id}> <Tr key={workflowIntegration.id}>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all"> <Td className="flex max-w-xs items-center gap-2 overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
<FontAwesomeIcon icon={faSlack} /> {workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK ? (
<div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div> <BsSlack />
) : (
<BsMicrosoftTeams />
)}
<div className="capitalize">
{workflowIntegration.integration.replaceAll("-", " ")}
</div>
{workflowIntegration.description && (
<Tooltip content={workflowIntegration.description}>
<FontAwesomeIcon icon={faInfoCircle} className="text-gray-400" size="sm" />
</Tooltip>
)}
</Td> </Td>
<Td>{workflowIntegration.slug}</Td> <Td>{workflowIntegration.slug}</Td>
<Td>{renderStatus(workflowIntegration.status)}</Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
@@ -159,29 +211,56 @@ export const OrgWorkflowIntegrationTab = withPermission(
> >
More details More details
</DropdownMenuItem> </DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionActions.Create}
an={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
triggerReinstall( {workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK && (
workflowIntegration.integration, <OrgPermissionCan
workflowIntegration.id I={OrgPermissionActions.Create}
); an={OrgPermissionSubjects.Settings}
}} >
> {(isAllowed) => (
Reinstall <DropdownMenuItem
</DropdownMenuItem> disabled={!isAllowed}
)} className={twMerge(
</OrgPermissionCan> !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async (e) => {
e.stopPropagation();
await triggerSlackReinstall(workflowIntegration.id);
}}
>
Reinstall
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
{workflowIntegration.integration ===
WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
an={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async (e) => {
e.stopPropagation();
await triggerMicrosoftTeamsInstallationStatusCheck(
workflowIntegration.id
);
}}
>
Check Installation Status
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Delete} I={OrgPermissionActions.Delete}
an={OrgPermissionSubjects.Settings} an={OrgPermissionSubjects.Settings}
@@ -11,7 +11,7 @@ const SettingsPageQueryParams = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/settings" "/_authenticate/_inject-org-details/_org-layout/organization/settings/"
)({ )({
component: SettingsPage, component: SettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams), validateSearch: zodValidator(SettingsPageQueryParams),
@@ -1,351 +1,163 @@
import { useEffect } from "react"; import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
import { Controller, useForm } from "react-hook-form"; import { faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
ContentLoader, DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState, EmptyState,
FormControl, Table,
Input, TableContainer,
Select, TBody,
SelectItem, Td,
Switch THead,
Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useGetSlackIntegrationChannels, useDeleteProjectWorkflowIntegration,
useGetSlackIntegrations, useGetWorkspaceWorkflowIntegrationConfig
useGetWorkspaceSlackConfig,
useUpdateProjectSlackConfig
} from "@app/hooks/api"; } from "@app/hooks/api";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
const formSchema = z.object({ import { AddWorkflowIntegrationModal } from "./components/AddWorkflowIntegrationModal";
slackIntegrationId: z.string(), import { EditWorkflowIntegrationModal } from "./components/EditWorkflowIntegrationModal";
isSecretRequestNotificationEnabled: z.boolean(), import { MicrosoftTeamsConfigRow } from "./components/MicrosoftTeamsConfigRow";
secretRequestChannels: z.string().array(), import { SlackConfigRow } from "./components/SlackConfigRow";
isAccessRequestNotificationEnabled: z.boolean(),
accessRequestChannels: z.string().array()
});
type TSlackConfigForm = z.infer<typeof formSchema>; export const renderProvider = (integration: WorkflowIntegrationPlatform) => {
if (integration === WorkflowIntegrationPlatform.SLACK) {
return <BsSlack />;
}
if (integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
return <BsMicrosoftTeams />;
}
return null;
};
export const WorkflowIntegrationTab = () => { export const WorkflowIntegrationTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"addWorkflowIntegration",
"removeIntegration",
"editIntegration"
] as const);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: slackConfig, isPending: isSlackConfigLoading } = useGetWorkspaceSlackConfig({ const { data: slackConfig, isPending: isSlackConfigLoading } =
workspaceId: currentWorkspace?.id ?? "" useGetWorkspaceWorkflowIntegrationConfig({
}); workspaceId: currentWorkspace?.id ?? "",
const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId); integration: WorkflowIntegrationPlatform.SLACK
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig(); });
const { const { data: microsoftTeamsConfig, isPending: isMicrosoftTeamsConfigLoading } =
control, useGetWorkspaceWorkflowIntegrationConfig({
watch, workspaceId: currentWorkspace?.id ?? "",
handleSubmit, integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
setValue, });
formState: { isDirty, isSubmitting }
} = useForm<TSlackConfigForm>({
resolver: zodResolver(formSchema),
defaultValues: {
isAccessRequestNotificationEnabled: false,
accessRequestChannels: [],
isSecretRequestNotificationEnabled: false,
secretRequestChannels: []
}
});
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled"); const { mutateAsync: deleteIntegration } = useDeleteProjectWorkflowIntegration();
const selectedSlackIntegrationId = watch("slackIntegrationId");
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId); const handleRemoveIntegration = async (
const slackChannelIdToName = Object.fromEntries( integrationType: WorkflowIntegrationPlatform,
(slackChannels || []).map((channel) => [channel.id, channel.name]) integrationId: string
); ) => {
const sortedSlackChannels = slackChannels?.sort((a, b) => if (!currentWorkspace.id) {
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
);
const handleIntegrationSave = async (data: TSlackConfigForm) => {
if (!currentWorkspace) {
return; return;
} }
await updateProjectSlackConfig({ await deleteIntegration({
workspaceId: currentWorkspace.id, projectId: currentWorkspace?.id ?? "",
...data, integration: integrationType,
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "), integrationId
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
}); });
createNotification({ createNotification({
type: "success", type: "success",
text: "Successfully updated slack integration" text: `Successfully removed ${integrationType.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase())} integration`
}); });
}; };
useEffect(() => { return (
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 <ContentLoader />;
}
return !slackIntegrations?.length ? (
<EmptyState title="You do not have any integrations configured.">
<Link
to="/organization/settings"
search={{
selectedTab: "workflow-integrations"
}}
>
<div className="mt-2 underline decoration-primary-800 underline-offset-4 duration-200 hover:cursor-pointer hover:text-mineshaft-100 hover:decoration-primary-600">
Create one now
</div>
</Link>
</EmptyState>
) : (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between"> <div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2> <p className="text-xl font-semibold text-mineshaft-100">Workflow Integrations</p>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
onClick={() => {
handlePopUpOpen("addWorkflowIntegration");
}}
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add
</Button>
)}
</OrgPermissionCan>
</div> </div>
<p className="mb-4 text-gray-400"> <p className="mb-4 text-gray-400">
This integration allows you to send notifications to your Slack workspace in response to Connect Infisical to other platforms for notification and workflow integrations.
events in your project.
</p> </p>
<form onSubmit={handleSubmit(handleIntegrationSave)}> <TableContainer>
<div className="max-w-md"> {!!slackConfig || !!microsoftTeamsConfig ? (
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}> <Table>
{(isAllowed) => ( <THead>
<Controller <Tr>
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( <Td>Provider</Td>
<FormControl errorText={error?.message} isError={Boolean(error)}> <Td>Access Request Notifications Destination</Td>
<Select <Td>Secret Request Notifications Destination</Td>
{...field} <Td />
isDisabled={!isAllowed} </Tr>
placeholder="None" </THead>
onValueChange={onChange} <TBody>
defaultValue={slackConfig?.slackIntegrationId} <SlackConfigRow
className="w-3/4 bg-mineshaft-600" handlePopUpOpen={handlePopUpOpen}
> isSlackConfigLoading={isSlackConfigLoading}
{slackIntegrations?.map((slackIntegration) => ( slackConfig={slackConfig}
<SelectItem
value={slackIntegration.id}
key={`slack-integration-${slackIntegration.id}`}
>
{slackIntegration.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="slackIntegrationId"
/> />
)} <MicrosoftTeamsConfigRow
</ProjectPermissionCan> handlePopUpOpen={handlePopUpOpen}
</div> isMicrosoftTeamsConfigLoading={isMicrosoftTeamsConfigLoading}
{selectedSlackIntegrationId && ( microsoftTeamsConfig={microsoftTeamsConfig}
<> />
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2> </TBody>
<Controller </Table>
control={control} ) : (
name="isSecretRequestNotificationEnabled" <div className="flex h-full w-full items-center justify-center">
render={({ field, fieldState: { error } }) => { <EmptyState
return ( title="No project workflow integrations configured. Add a new workflow integration to get started"
<FormControl icon={faGear}
isError={Boolean(error)}
errorText={error?.message}
className="mb-2 mt-3"
>
<Switch
id="secret-approval-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Secret Approval Requests</p>
</Switch>
</FormControl>
);
}}
/> />
{secretRequestNotifState && ( </div>
<Controller
control={control}
name="secretRequestChannels"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Slack channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value
?.filter(Boolean)
.map((entry) => slackChannelIdToName[entry])
.join(", ")}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{sortedSlackChannels?.map((slackChannel) => {
const isChecked = value?.includes(slackChannel.id);
return (
<DropdownMenuItem
onClick={(evt) => {
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 && <FontAwesomeIcon icon={faCheckCircle} />}
>
{slackChannel.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="isAccessRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
<Switch
id="access-request-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Access Requests</p>
</Switch>
</FormControl>
);
}}
/>
{accessRequestNotifState && (
<Controller
control={control}
name="accessRequestChannels"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Slack channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value
?.filter(Boolean)
.map((entry) => slackChannelIdToName[entry])
.join(", ")}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{sortedSlackChannels?.map((slackChannel) => {
const isChecked = value?.includes(slackChannel.id);
return (
<DropdownMenuItem
onClick={(evt) => {
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 && <FontAwesomeIcon icon={faCheckCircle} />}
>
{slackChannel.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
<Button
colorSchema="secondary"
className="mt-4"
type="submit"
isDisabled={!isDirty}
isLoading={isSubmitting}
>
Save
</Button>
</>
)} )}
</form> </TableContainer>
<AddWorkflowIntegrationModal
isOpen={popUp.addWorkflowIntegration.isOpen}
onToggle={(state) => handlePopUpToggle("addWorkflowIntegration", state)}
/>
<DeleteActionModal
isOpen={popUp.removeIntegration.isOpen}
title="Are you sure want to remove this integration?"
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await handleRemoveIntegration(
popUp.removeIntegration.data?.integration,
popUp.removeIntegration.data?.integrationId
);
handlePopUpClose("removeIntegration");
}}
/>
<EditWorkflowIntegrationModal
isOpen={popUp.editIntegration.isOpen}
onClose={() => handlePopUpClose("editIntegration")}
integration={popUp.editIntegration.data?.integration}
/>
</div> </div>
); );
}; };
@@ -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: <FontAwesomeIcon icon={faSlack} size="lg" />,
platform: WorkflowIntegrationPlatform.SLACK,
title: "Slack"
},
{
icon: <BsMicrosoftTeams className="text-lg" />,
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
title: "Microsoft Teams"
}
];
export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform);
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(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 (
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
<ModalContent
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
className="my-4"
>
<AnimatePresence mode="wait">
{wizardStep === WizardSteps.SelectPlatform && (
<motion.div
key="select-type-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<div className="mb-4 text-mineshaft-300">Select a workflow integration</div>
<div className="flex items-center space-x-4">
{PLATFORM_LIST.map(({ icon, platform, title }) => {
const isConfigured =
(platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS &&
microsoftTeamsConfigured) ||
(platform === WorkflowIntegrationPlatform.SLACK && slackConfigured);
return (
<div className="relative" key={platform}>
<div
className={twMerge(
"flex h-32 w-36 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white",
isConfigured && "border-primary/50 opacity-60"
)}
role="button"
tabIndex={0}
onClick={() => {
setSelectedPlatform(platform);
setWizardStep(WizardSteps.PlatformInputs);
}}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
setSelectedPlatform(platform);
setWizardStep(WizardSteps.PlatformInputs);
}
}}
>
<div>{icon}</div>
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
</div>
{isConfigured && (
<div className="absolute bottom-0 left-0 right-0 z-30 h-full">
<div className="relative h-full">
<div className="absolute bottom-0 right-0 w-full flex-row items-center overflow-hidden whitespace-nowrap rounded-b-md bg-primary px-2 py-0.5 text-center text-xs text-black">
Already Configured
</div>
</div>
</div>
)}
</div>
);
})}
</div>
</motion.div>
)}
{wizardStep === WizardSteps.PlatformInputs &&
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
<motion.div
key="platform-inputs-step-slack"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<SlackIntegrationForm onClose={() => handleFormReset(false)} />
</motion.div>
)}
{wizardStep === WizardSteps.PlatformInputs &&
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<motion.div
key="platform-inputs-step-ms-teams"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<MicrosoftTeamsIntegrationForm onClose={() => handleFormReset(false)} />
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>
);
};
@@ -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 (
<Modal isOpen={isOpen} onOpenChange={handleFormReset}>
<ModalContent
title={`Edit ${integration?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
>
{integration === WorkflowIntegrationPlatform.SLACK && (
<SlackIntegrationForm onClose={handleFormReset} />
)}
{integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
<MicrosoftTeamsIntegrationForm onClose={handleFormReset} />
)}
</ModalContent>
</Modal>
);
};
@@ -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 (
<Tr>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis">
<div className="flex items-center gap-2">
<BsMicrosoftTeams />
Microsoft Teams
</div>
</Td>
<Td>
{microsoftTeamsConfig.isAccessRequestNotificationEnabled &&
!isLoadingConfig &&
microsoftTeamsConfig.accessRequestChannels?.channelIds?.length > 0 ? (
<Badge>
{microsoftTeamsConfig.accessRequestChannels.channelIds
.map((channel) => microsoftTeamsChannelIdToName[channel])
.join(", ")}
</Badge>
) : isLoadingConfig ? (
<Spinner size="xs" />
) : (
<Badge variant="danger">Disabled</Badge>
)}
</Td>
<Td>
{microsoftTeamsConfig.isSecretRequestNotificationEnabled &&
!isLoadingConfig &&
microsoftTeamsConfig.secretRequestChannels?.channelIds?.length > 0 ? (
<Badge>
{microsoftTeamsConfig.secretRequestChannels.channelIds
.map((channel) => microsoftTeamsChannelIdToName[channel])
.join(", ")}
</Badge>
) : isLoadingConfig ? (
<Spinner size="xs" />
) : (
<Badge variant="danger">Disabled</Badge>
)}
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("editIntegration", {
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
});
}}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeIntegration", {
integrationId: microsoftTeamsConfig.integrationId,
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
});
}}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
};
@@ -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<typeof formSchema>;
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<TMicrosoftTeamsConfigForm>({
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<string, string>
);
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 (
<form onSubmit={handleSubmit(handleIntegrationSave)}>
<div className="flex w-full flex-col justify-start">
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Controller
control={control}
name="microsoftTeamsIntegrationId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams Integration"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
{...field}
isDisabled={!isAllowed}
placeholder="None"
onValueChange={(v) => {
onChange(v);
setValue("isAccessRequestNotificationEnabled", false);
setValue("isSecretRequestNotificationEnabled", false);
setValue("accessRequestChannels", {
teamId: "",
channelIds: []
});
setValue("secretRequestChannels", {
teamId: "",
channelIds: []
});
}}
className="w-full"
defaultValue={microsoftTeamsConfig?.integrationId}
>
{microsoftTeamsIntegrations?.map((microsoftTeamsIntegration) => (
<SelectItem
value={microsoftTeamsIntegration.id}
key={`microsoft-teams-integration-${microsoftTeamsIntegration.id}`}
>
{microsoftTeamsIntegration.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
</ProjectPermissionCan>
</div>
{selectedMicrosoftTeamsIntegrationId && (
<>
<h2 className="mb-2 flex-1 text-sm text-mineshaft-400">Configure Events</h2>
<Controller
control={control}
name="isSecretRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mb-2 mt-3"
>
<Switch
id="secret-approval-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Secret Approval Requests</p>
</Switch>
</FormControl>
);
}}
/>
{secretRequestNotificationsEnabled && (
<>
<Controller
control={control}
name="secretRequestChannels.teamId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Secret Approval Requests Notifications Team"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
placeholder={
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
}
className="w-full"
onValueChange={(value) => onChange(value)}
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
>
{!isLoadingMicrosoftTeamsIntegrationTeams &&
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
<SelectItem
key={`secret-requests-microsoft-teams-team-${team.teamId}`}
value={team.teamId}
>
{team.teamName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretRequestChannels.channelIds"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger
className={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
? "opacity-50"
: ""
}
asChild
disabled={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
}
>
{selectedSecretRequestTeamId ? (
<Input
isReadOnly
value={
isLoadingMicrosoftTeamsIntegrationTeams
? "Loading..."
: value
?.filter(Boolean)
.map((entry) => channelIdToName[entry])
.join(", ")
}
className="text-left"
/>
) : (
<Input
isReadOnly
value="Select a team..."
className="py-2 text-left text-sm"
/>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{selectableSecretRequestChannelIds?.map((channel) => {
const isChecked = value?.includes(channel.channelId);
return (
<DropdownMenuItem
onClick={(evt) => {
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 && <FontAwesomeIcon icon={faCheckCircle} />}
>
{channel.channelName}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="isAccessRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
<Switch
id="access-request-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Access Requests</p>
</Switch>
</FormControl>
);
}}
/>
{accessRequestNotificationsEnabled && (
<>
<Controller
control={control}
name="accessRequestChannels.teamId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Access Requests Notifications Team"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
placeholder={
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
}
className="w-full"
onValueChange={(value) => onChange(value)}
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
>
{!isLoadingMicrosoftTeamsIntegrationTeams &&
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
<SelectItem
key={`access-requests-microsoft-teams-team-${team.teamId}`}
value={team.teamId}
>
{team.teamName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="accessRequestChannels.channelIds"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Microsoft Teams channels"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger
className={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
? "opacity-50"
: ""
}
asChild
disabled={
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
}
>
{selectedAccessRequestTeamId ? (
<Input
isReadOnly
value={
isLoadingMicrosoftTeamsIntegrationTeams
? "Loading..."
: value
?.filter(Boolean)
.map((entry) => channelIdToName[entry])
.join(", ")
}
className="text-left"
/>
) : (
<Input
isReadOnly
value="Select a team..."
className="py-2 text-left text-sm"
/>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start"
>
{selectableAccessRequestChannelIds?.map((channel) => {
const isChecked = value?.includes(channel.channelId);
return (
<DropdownMenuItem
onClick={(evt) => {
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 && <FontAwesomeIcon icon={faCheckCircle} />}
>
{channel.channelName}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</>
)}
<Button
colorSchema="secondary"
className="mt-4"
type="submit"
disabled={!isDirty}
isDisabled={!isDirty}
isLoading={isSubmitting}
>
Save
</Button>
</>
)}
</form>
);
};

Some files were not shown because too many files have changed in this diff Show More