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

View File

@@ -59,6 +59,7 @@
"axios": "^1.6.7",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
"connect-redis": "^7.1.1",
@@ -2358,12 +2359,13 @@
}
},
"node_modules/@azure/core-auth": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz",
"integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.1.0",
"@azure/core-util": "^1.11.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -2518,14 +2520,15 @@
}
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.1.tgz",
"integrity": "sha512-ExPSbgjwCoht6kB7B4MeZoBAxcQSIl29r/bPeazZJx50ej4JJCByimLOrZoIsurISNyJQQHf30b3JfqC3Hb88A==",
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz",
"integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.4.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.9.0",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
@@ -2602,9 +2605,10 @@
}
},
"node_modules/@azure/core-util": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz",
"integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
"integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"tslib": "^2.6.2"
@@ -2625,46 +2629,60 @@
}
},
"node_modules/@azure/identity": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.3.0.tgz",
"integrity": "sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz",
"integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==",
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.5.0",
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.9.0",
"@azure/core-client": "^1.9.2",
"@azure/core-rest-pipeline": "^1.1.0",
"@azure/core-rest-pipeline": "^1.17.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.3.0",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"@azure/msal-browser": "^3.11.1",
"@azure/msal-node": "^2.9.2",
"events": "^3.0.0",
"jws": "^4.0.0",
"open": "^8.0.0",
"stoppable": "^1.1.0",
"@azure/msal-browser": "^4.2.0",
"@azure/msal-node": "^3.5.0",
"open": "^10.1.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/identity/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"node_modules/@azure/identity/node_modules/@azure/abort-controller": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/identity/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"node_modules/@azure/identity/node_modules/@azure/msal-node": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz",
"integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
"@azure/msal-common": "15.5.1",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@azure/identity/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@azure/keyvault-keys": {
@@ -2700,30 +2718,33 @@
}
},
"node_modules/@azure/msal-browser": {
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.18.0.tgz",
"integrity": "sha512-jvK5bDUWbpOaJt2Io/rjcaOVcUzkqkrCme/WntdV1SMUc67AiTcEdKuY6G/nMQ7N5Cfsk9SfpugflQwDku53yg==",
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz",
"integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "14.13.0"
"@azure/msal-common": "15.5.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "14.13.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.13.0.tgz",
"integrity": "sha512-b4M/tqRzJ4jGU91BiwCsLTqChveUEyFK3qY2wGfZ0zBswIBZjAxopx5CYt5wzZFKuN15HqRDYXQbztttuIC3nA==",
"version": "15.5.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz",
"integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.10.0.tgz",
"integrity": "sha512-JxsSE0464a8IA/+q5EHKmchwNyUFJHtCH00tSXsLaOddwLjG6yVvTH6lGgPcWMhO7YWUXj/XVgVgeE9kZtsPUQ==",
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz",
"integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "14.13.0",
"@azure/msal-common": "14.16.0",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
@@ -2731,6 +2752,15 @@
"node": ">=16"
}
},
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
"version": "14.16.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
"integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -10011,9 +10041,10 @@
"dev": true
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
"integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz",
"integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -10398,6 +10429,15 @@
"@types/webidl-conversions": "*"
}
},
"node_modules/@types/ws": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/xml-encryption": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz",
@@ -11150,6 +11190,12 @@
"node": ">=0.4.0"
}
},
"node_modules/adaptivecards": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/adaptivecards/-/adaptivecards-1.2.3.tgz",
"integrity": "sha512-amQ5OSW3OpIkrxVKLjxVBPk/T49yuOtnqs1z5ZPfZr0+OpTovzmiHbyoAGDIsu5SNYHwOZFp/3LGOnRaALFa/g==",
"license": "MIT"
},
"node_modules/adm-zip": {
"version": "0.5.12",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
@@ -12012,6 +12058,245 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/botbuilder": {
"version": "4.23.2",
"resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.2.tgz",
"integrity": "sha512-E3UjkPlAmT8TidZIAW1ucVRejz0KBbWEn0wNxJ37GncPl8txhmWvs21xn3hBrifSR4++Y6q/hp/A5cHFQcFGJw==",
"license": "MIT",
"dependencies": {
"@azure/core-http": "^3.0.4",
"@azure/msal-node": "^2.13.1",
"axios": "^1.7.7",
"botbuilder-core": "4.23.2",
"botbuilder-stdlib": "4.23.2-internal",
"botframework-connector": "4.23.2",
"botframework-schema": "4.23.2",
"botframework-streaming": "4.23.2",
"dayjs": "^1.11.13",
"filenamify": "^6.0.0",
"fs-extra": "^11.2.0",
"htmlparser2": "^9.0.1",
"uuid": "^10.0.0",
"zod": "^3.23.8"
}
},
"node_modules/botbuilder-core": {
"version": "4.23.2",
"resolved": "https://registry.npmjs.org/botbuilder-core/-/botbuilder-core-4.23.2.tgz",
"integrity": "sha512-GwrfkfbEJqCLnhDVc6uKlzKtrptfYTxQxHYfF22s1AxTKdTiA9vsDN9rXq8We7QUPXFOF1ylF1e87k0fQ3Sf+A==",
"license": "MIT",
"dependencies": {
"botbuilder-dialogs-adaptive-runtime-core": "4.23.2-preview",
"botbuilder-stdlib": "4.23.2-internal",
"botframework-connector": "4.23.2",
"botframework-schema": "4.23.2",
"uuid": "^10.0.0",
"zod": "^3.23.8"
}
},
"node_modules/botbuilder-core/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/botbuilder-dialogs-adaptive-runtime-core": {
"version": "4.23.2-preview",
"resolved": "https://registry.npmjs.org/botbuilder-dialogs-adaptive-runtime-core/-/botbuilder-dialogs-adaptive-runtime-core-4.23.2-preview.tgz",
"integrity": "sha512-+b5oHSDNodYXPnQbub+hTNmQLtBB4hj/ZW73g4Sqv5oAdqHoK/dX181UpiFAvDpHGe8Kx3SNYtRHJIj71u4t0Q==",
"license": "MIT",
"dependencies": {
"dependency-graph": "^1.0.0"
}
},
"node_modules/botbuilder-stdlib": {
"version": "4.23.2-internal",
"resolved": "https://registry.npmjs.org/botbuilder-stdlib/-/botbuilder-stdlib-4.23.2-internal.tgz",
"integrity": "sha512-5WAu59gCZX3lz2NNw28q+IlAAFIQjXij0wXmN8qh+Tg4PQOCl+5P3hoYqcHIWtGd5Kgn+dpaHtBIewl2LaOXKQ==",
"license": "MIT"
},
"node_modules/botbuilder/node_modules/fs-extra": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/botbuilder/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/botframework-connector": {
"version": "4.23.2",
"resolved": "https://registry.npmjs.org/botframework-connector/-/botframework-connector-4.23.2.tgz",
"integrity": "sha512-G4gDpEHhA8AUKbgHMJ1LUjsuDlRPFEcXnH8ouxLI0opT2p1LcUSAAgS4hoOrkaylr04zxrUI0nEWkuWDiWDwzw==",
"license": "MIT",
"dependencies": {
"@azure/core-http": "^3.0.4",
"@azure/identity": "^4.4.1",
"@azure/msal-node": "^2.13.1",
"@types/jsonwebtoken": "9.0.6",
"axios": "^1.7.7",
"base64url": "^3.0.0",
"botbuilder-stdlib": "4.23.2-internal",
"botframework-schema": "4.23.2",
"buffer": "^6.0.3",
"cross-fetch": "^4.0.0",
"https-proxy-agent": "^7.0.5",
"jsonwebtoken": "^9.0.2",
"node-fetch": "^2.7.0",
"openssl-wrapper": "^0.3.4",
"rsa-pem-from-mod-exp": "^0.8.6",
"zod": "^3.23.8"
}
},
"node_modules/botframework-connector/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/botframework-connector/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/botframework-connector/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/botframework-schema": {
"version": "4.23.2",
"resolved": "https://registry.npmjs.org/botframework-schema/-/botframework-schema-4.23.2.tgz",
"integrity": "sha512-eO1fmvfCEVJfnqNNAerQU8CHp0FMYTyE459ztNx2k1QJYMl/ds+LNNkGIUlQQFsdVbi2umadK+6hL2a9kqXMqQ==",
"license": "MIT",
"dependencies": {
"adaptivecards": "1.2.3",
"uuid": "^10.0.0",
"zod": "^3.23.8"
}
},
"node_modules/botframework-schema/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/botframework-streaming": {
"version": "4.23.2",
"resolved": "https://registry.npmjs.org/botframework-streaming/-/botframework-streaming-4.23.2.tgz",
"integrity": "sha512-UBF0puC2RX8Z0dkN/ag9BuSNWdB5MUtobZLzeaH1h5t7QAYAVNk/SrsUgkBwUsqWpwaZqU+vrGOeByLShDcvaQ==",
"license": "MIT",
"dependencies": {
"@types/node": "18.19.47",
"@types/ws": "^6.0.3",
"uuid": "^10.0.0",
"ws": "^7.5.10"
}
},
"node_modules/botframework-streaming/node_modules/@types/node": {
"version": "18.19.47",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz",
"integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/botframework-streaming/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/botframework-streaming/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/botframework-streaming/node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
@@ -12139,6 +12424,21 @@
"uuid": "^9.0.0"
}
},
"node_modules/bundle-name": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
"license": "MIT",
"dependencies": {
"run-applescript": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bundle-require": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz",
@@ -12791,6 +13091,15 @@
"node": ">=12.0.0"
}
},
"node_modules/cross-fetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.7.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -12872,6 +13181,12 @@
"node": "*"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/dc-polyfill": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz",
@@ -13011,6 +13326,34 @@
"node": ">=0.10.0"
}
},
"node_modules/default-browser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
"license": "MIT",
"dependencies": {
"bundle-name": "^4.1.0",
"default-browser-id": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/default-browser-id": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -13029,11 +13372,15 @@
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"license": "MIT",
"engines": {
"node": ">=8"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-properties": {
@@ -13093,6 +13440,15 @@
"node": ">= 0.8"
}
},
"node_modules/dependency-graph": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",
"integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
@@ -13166,6 +13522,47 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
@@ -13174,6 +13571,20 @@
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
@@ -14494,6 +14905,33 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/filename-reserved-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz",
"integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/filenamify": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz",
"integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==",
"license": "MIT",
"dependencies": {
"filename-reserved-regex": "^3.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -15678,6 +16116,25 @@
],
"license": "MIT"
},
"node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -16191,6 +16648,39 @@
"node": ">=0.10.0"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
"license": "MIT",
"dependencies": {
"is-docker": "^3.0.0"
},
"bin": {
"is-inside-container": "cli.js"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-inside-container/node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-interactive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
@@ -16718,7 +17208,6 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"dependencies": {
"universalify": "^2.0.0"
},
@@ -18767,16 +19256,33 @@
}
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz",
"integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==",
"license": "MIT",
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
"default-browser": "^5.2.1",
"define-lazy-prop": "^3.0.0",
"is-inside-container": "^1.0.0",
"is-wsl": "^3.1.0"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open/node_modules/is-wsl": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
"license": "MIT",
"dependencies": {
"is-inside-container": "^1.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -18801,6 +19307,12 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/openssl-wrapper": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/openssl-wrapper/-/openssl-wrapper-0.3.4.tgz",
"integrity": "sha512-iITsrx6Ho8V3/2OVtmZzzX8wQaKAaFXEJQdzoPUZDtyf5jWFlqo+h+OhGT4TATQ47f9ACKHua8nw7Qoy85aeKQ==",
"license": "MIT"
},
"node_modules/opentracing": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz",
@@ -20854,6 +21366,24 @@
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="
},
"node_modules/rsa-pem-from-mod-exp": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.6.tgz",
"integrity": "sha512-c5ouQkOvGHF1qomUUDJGFcXsomeSO2gbEs6hVhMAtlkE1CuaZase/WzoaKFG/EZQuNmq6pw/EMCeEnDvOgCJYQ==",
"license": "MIT"
},
"node_modules/run-applescript": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
"integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -21728,15 +22258,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stoppable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
"engines": {
"node": ">=4",
"npm": ">=6"
}
},
"node_modules/stream-events": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
@@ -23520,7 +24041,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
@@ -24951,9 +25471,10 @@
}
},
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"version": "3.24.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -175,6 +175,7 @@
"axios": "^1.6.7",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
"connect-redis": "^7.1.1",

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 { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
declare module "@fastify/request-context" {
interface RequestContextData {
@@ -246,6 +247,7 @@ declare module "fastify" {
kmipOperation: TKmipOperationServiceFactory;
gateway: TGatewayServiceFactory;
secretRotationV2: TSecretRotationV2ServiceFactory;
microsoftTeams: TMicrosoftTeamsServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory;
};

View File

@@ -426,6 +426,16 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
import {
TMicrosoftTeamsIntegrations,
TMicrosoftTeamsIntegrationsInsert,
TMicrosoftTeamsIntegrationsUpdate
} from "@app/db/schemas/microsoft-teams-integrations";
import {
TProjectMicrosoftTeamsConfigs,
TProjectMicrosoftTeamsConfigsInsert,
TProjectMicrosoftTeamsConfigsUpdate
} from "@app/db/schemas/project-microsoft-teams-configs";
import {
TSecretReminderRecipients,
TSecretReminderRecipientsInsert,
@@ -1002,6 +1012,16 @@ declare module "knex/types/tables" {
TSecretRotationV2SecretMappingsInsert,
TSecretRotationV2SecretMappingsUpdate
>;
[TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType<
TMicrosoftTeamsIntegrations,
TMicrosoftTeamsIntegrationsInsert,
TMicrosoftTeamsIntegrationsUpdate
>;
[TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType<
TProjectMicrosoftTeamsConfigs,
TProjectMicrosoftTeamsConfigsInsert,
TProjectMicrosoftTeamsConfigsUpdate
>;
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
TSecretReminderRecipients,
TSecretReminderRecipientsInsert,

View File

@@ -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);
}
}

View File

@@ -58,6 +58,7 @@ export * from "./kms-keys";
export * from "./kms-root-config";
export * from "./ldap-configs";
export * from "./ldap-group-maps";
export * from "./microsoft-teams-integrations";
export * from "./models";
export * from "./oidc-configs";
export * from "./org-bots";

View File

@@ -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>
>;

View File

@@ -147,6 +147,8 @@ export enum TableName {
KmipClientCertificates = "kmip_client_certificates",
SecretRotationV2 = "secret_rotations_v2",
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
SecretReminderRecipients = "secret_reminder_recipients",
GithubOrgSyncConfig = "github_org_sync_configs"
}

View File

@@ -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>
>;

View File

@@ -26,7 +26,10 @@ export const SuperAdminSchema = z.object({
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
authConsentContent: z.string().nullable().optional(),
pageFrameContent: z.string().nullable().optional(),
adminIdentityIds: z.string().array().nullable().optional()
adminIdentityIds: z.string().array().nullable().optional(),
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@@ -14,7 +14,8 @@ export const WorkflowIntegrationsSchema = z.object({
orgId: z.string().uuid(),
description: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
status: z.string().default("installed")
});
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;

View File

@@ -6,13 +6,15 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -67,6 +69,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
};
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
@@ -84,6 +88,8 @@ export const accessApprovalRequestServiceFactory = ({
smtpService,
userDAL,
kmsService,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL,
projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const createAccessApprovalRequest = async ({
@@ -219,24 +225,30 @@ export const accessApprovalRequestServiceFactory = ({
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
await triggerSlackNotification({
projectId: project.id,
projectSlackConfigDAL,
projectDAL,
kmsService,
notification: {
type: SlackTriggerFeature.ACCESS_REQUEST,
payload: {
projectName: project.name,
requesterFullName,
isTemporary,
requesterEmail: requestedByUser.email as string,
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl,
note
}
await triggerWorkflowIntegrationNotification({
input: {
notification: {
type: TriggerFeature.ACCESS_REQUEST,
payload: {
projectName: project.name,
requesterFullName,
isTemporary,
requesterEmail: requestedByUser.email as string,
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl,
note
}
},
projectId: project.id
},
dependencies: {
projectDAL,
projectSlackConfigDAL,
kmsService,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL
}
});

View File

@@ -29,6 +29,7 @@ import {
TSecretSyncRaw,
TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types";
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
import { KmipPermission } from "../kmip/kmip-enum";
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
@@ -244,11 +245,14 @@ export enum EventType {
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_SLACK_INTEGRATION = "get-slack-integration",
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
INTEGRATION_SYNCED = "integration-synced",
@@ -321,6 +325,15 @@ export enum EventType {
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
PROJECT_ACCESS_REQUEST = "project-access-request",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
}
@@ -1980,22 +1993,24 @@ interface GetSlackIntegration {
};
}
interface UpdateProjectSlackConfig {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
interface UpdateProjectWorkflowIntegrationConfig {
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: {
id: string;
slackIntegrationId: string;
integrationId: string;
integration: WorkflowIntegration;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
};
}
interface GetProjectSlackConfig {
type: EventType.GET_PROJECT_SLACK_CONFIG;
interface GetProjectWorkflowIntegrationConfig {
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
metadata: {
id: string;
integration: WorkflowIntegration;
};
}
@@ -2561,6 +2576,66 @@ interface RotateSecretRotationEvent {
};
}
interface MicrosoftTeamsWorkflowIntegrationCreateEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE;
metadata: {
tenantId: string;
slug: string;
description?: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationDeleteEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE;
metadata: {
tenantId: string;
id: string;
slug: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS;
metadata: {
tenantId: string;
slug: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS;
metadata: {
tenantId: string;
slug: string;
id: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationGetEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET;
metadata: {
tenantId: string;
slug: string;
id: string;
};
}
interface MicrosoftTeamsWorkflowIntegrationListEvent {
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST;
metadata: Record<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 =
| GetSecretsEvent
| GetSecretEvent
@@ -2723,8 +2798,8 @@ export type Event =
| UpdateSlackIntegration
| DeleteSlackIntegration
| GetSlackIntegration
| UpdateProjectSlackConfig
| GetProjectSlackConfig
| UpdateProjectWorkflowIntegrationConfig
| GetProjectWorkflowIntegrationConfig
| GetProjectSshConfig
| UpdateProjectSshConfig
| IntegrationSyncedEvent
@@ -2794,4 +2869,11 @@ export type Event =
| CreateSecretRotationEvent
| UpdateSecretRotationEvent
| DeleteSecretRotationEvent
| RotateSecretRotationEvent;
| RotateSecretRotationEvent
| MicrosoftTeamsWorkflowIntegrationCreateEvent
| MicrosoftTeamsWorkflowIntegrationDeleteEvent
| MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
| MicrosoftTeamsWorkflowIntegrationGetEvent
| MicrosoftTeamsWorkflowIntegrationListEvent
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;

View File

@@ -17,9 +17,13 @@ import { groupBy, pick, unique } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types";
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -52,8 +56,6 @@ import {
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -126,6 +128,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
};
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -155,7 +159,9 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionTagV2BridgeDAL,
licenseService,
projectSlackConfigDAL,
resourceMetadataDAL
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@@ -1171,21 +1177,28 @@ export const secretApprovalRequestServiceFactory = ({
const env = await projectEnvDAL.findOne({ id: policy.envId });
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
await triggerSlackNotification({
projectId,
projectDAL,
kmsService,
projectSlackConfigDAL,
notification: {
type: SlackTriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
await triggerWorkflowIntegrationNotification({
input: {
projectId,
notification: {
type: TriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
}
}
},
dependencies: {
projectDAL,
projectSlackConfigDAL,
kmsService,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
}
});
@@ -1503,21 +1516,28 @@ export const secretApprovalRequestServiceFactory = ({
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
const env = await projectEnvDAL.findOne({ id: policy.envId });
await triggerSlackNotification({
projectId,
projectDAL,
kmsService,
projectSlackConfigDAL,
notification: {
type: SlackTriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
await triggerWorkflowIntegrationNotification({
input: {
projectId,
notification: {
type: TriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
}
}
},
dependencies: {
projectDAL,
kmsService,
projectSlackConfigDAL,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL
}
});

View File

@@ -20,7 +20,7 @@ export const BaseSecretRotationSchema = (type: SecretRotation) =>
// unique to provider
type: true,
parameters: true,
secretMappings: true
secretsMapping: true
}).extend({
connection: z.object({
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),

View File

@@ -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");
}
};

View File

@@ -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">;
};
};

View File

@@ -111,6 +111,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
return;
}
// Authentication is handled on a route-level here.
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
return;
}
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
if (!authMode) return;

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 { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
import { kmsServiceFactory } from "@app/services/kms/kms-service";
import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal";
import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
import { orgDALFactory } from "@app/services/org/org-dal";
@@ -426,6 +429,8 @@ export const registerRoutes = async (
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@@ -687,6 +692,15 @@ export const registerRoutes = async (
orgDAL,
externalGroupOrgRoleMappingDAL
});
const microsoftTeamsService = microsoftTeamsServiceFactory({
microsoftTeamsIntegrationDAL,
permissionService,
workflowIntegrationDAL,
kmsService,
serverCfgDAL: superAdminDAL
});
const superAdminService = superAdminServiceFactory({
userDAL,
identityDAL,
@@ -700,7 +714,8 @@ export const registerRoutes = async (
orgService,
keyStore,
licenseService,
kmsService
kmsService,
microsoftTeamsService
});
const orgAdminService = orgAdminServiceFactory({
@@ -1026,6 +1041,8 @@ export const registerRoutes = async (
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsIntegrationDAL,
projectTemplateService,
groupProjectDAL,
smtpService
@@ -1150,7 +1167,9 @@ export const registerRoutes = async (
userDAL,
licenseService,
projectSlackConfigDAL,
resourceMetadataDAL
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService
});
const secretService = secretServiceFactory({
@@ -1212,7 +1231,9 @@ export const registerRoutes = async (
accessApprovalPolicyApproverDAL,
projectSlackConfigDAL,
kmsService,
groupDAL
groupDAL,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL
});
const secretReplicationService = secretReplicationServiceFactory({
@@ -1610,6 +1631,7 @@ export const registerRoutes = async (
await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
await kmsService.startService();
await microsoftTeamsService.start();
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
@@ -1702,6 +1724,7 @@ export const registerRoutes = async (
kmipOperation: kmipOperationService,
gateway: gatewayService,
secretRotationV2: secretRotationV2Service,
microsoftTeams: microsoftTeamsService,
assumePrivileges: assumePrivilegeService,
githubOrgSync: githubOrgSyncConfigService
});

View File

@@ -27,7 +27,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
createdAt: true,
updatedAt: true,
encryptedSlackClientId: true,
encryptedSlackClientSecret: true
encryptedSlackClientSecret: true,
encryptedMicrosoftTeamsAppId: true,
encryptedMicrosoftTeamsClientSecret: true,
encryptedMicrosoftTeamsBotId: true
}).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
@@ -74,6 +77,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}),
slackClientId: z.string().optional(),
slackClientSecret: z.string().optional(),
microsoftTeamsAppId: z.string().optional(),
microsoftTeamsClientSecret: z.string().optional(),
microsoftTeamsBotId: z.string().optional(),
authConsentContent: z
.string()
.trim()
@@ -197,15 +203,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/integrations/slack/config",
url: "/integrations",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
clientId: z.string(),
clientSecret: z.string()
slack: z.object({
clientId: z.string(),
clientSecret: z.string()
}),
microsoftTeams: z.object({
appId: z.string(),
clientSecret: z.string(),
botId: z.string()
})
})
}
},
@@ -215,9 +228,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
});
},
handler: async () => {
const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig();
const adminIntegrationsConfig = await server.services.superAdmin.getAdminIntegrationsConfig();
return adminSlackConfig;
return adminIntegrationsConfig;
}
});

View File

@@ -47,6 +47,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router";
import { registerUserRouter } from "./user-router";
import { registerWebhookRouter } from "./webhook-router";
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" });
@@ -79,6 +80,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (workflowIntegrationRouter) => {
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
await workflowIntegrationRouter.register(registerMicrosoftTeamsRouter, { prefix: "/microsoft-teams" });
},
{ prefix: "/workflow-integrations" }
);

View File

@@ -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);
}
});
};

View File

@@ -14,6 +14,7 @@ import {
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
@@ -21,8 +22,10 @@ import { re2Validator } from "@app/lib/zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
@@ -740,55 +743,112 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/slack-config",
url: "/:workspaceId/workflow-integration-config/:integration",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
workspaceId: z.string().trim(),
integration: z.nativeEnum(WorkflowIntegration)
}),
response: {
200: ProjectSlackConfigsSchema.pick({
id: true,
slackIntegrationId: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
200: z.discriminatedUnion("integration", [
ProjectSlackConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
integrationId: z.string()
})
),
ProjectMicrosoftTeamsConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string()
})
)
])
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const config = await server.services.project.getProjectWorkflowIntegrationConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
integration: req.params.integration
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
metadata: {
id: config.id,
integration: config.integration
}
}
});
return config;
}
});
server.route({
method: "DELETE",
url: "/:projectId/workflow-integration/:integration/:integrationId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
projectId: z.string().trim(),
integration: z.nativeEnum(WorkflowIntegration),
integrationId: z.string()
}),
response: {
200: z.object({
integrationConfig: z.object({
id: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const slackConfig = await server.services.project.getProjectSlackConfig({
const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
projectId: req.params.projectId,
integration: req.params.integration,
integrationId: req.params.integrationId
});
if (slackConfig) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_SLACK_CONFIG,
metadata: {
id: slackConfig.id
}
}
});
}
return slackConfig;
return {
integrationConfig: deletedIntegration
};
}
});
server.route({
method: "PUT",
url: "/:workspaceId/slack-config",
url: "/:workspaceId/workflow-integration",
config: {
rateLimit: readLimit
},
@@ -796,27 +856,57 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
slackIntegrationId: z.string(),
isAccessRequestNotificationEnabled: z.boolean(),
accessRequestChannels: validateSlackChannelsField,
isSecretRequestNotificationEnabled: z.boolean(),
secretRequestChannels: validateSlackChannelsField
}),
response: {
200: ProjectSlackConfigsSchema.pick({
id: true,
slackIntegrationId: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
body: z.discriminatedUnion("integration", [
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
integrationId: z.string(),
accessRequestChannels: validateSlackChannelsField,
secretRequestChannels: validateSlackChannelsField,
isAccessRequestNotificationEnabled: z.boolean(),
isSecretRequestNotificationEnabled: z.boolean()
}),
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string(),
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
secretRequestChannels: validateMicrosoftTeamsChannelsSchema,
isAccessRequestNotificationEnabled: z.boolean(),
isSecretRequestNotificationEnabled: z.boolean()
})
]),
response: {
200: z.discriminatedUnion("integration", [
ProjectSlackConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
integrationId: z.string()
})
),
ProjectMicrosoftTeamsConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
isSecretRequestNotificationEnabled: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string(),
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
secretRequestChannels: validateMicrosoftTeamsChannelsSchema
})
)
])
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const slackConfig = await server.services.project.updateProjectSlackConfig({
const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
@@ -829,19 +919,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG,
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
metadata: {
id: slackConfig.id,
slackIntegrationId: slackConfig.slackIntegrationId,
isAccessRequestNotificationEnabled: slackConfig.isAccessRequestNotificationEnabled,
accessRequestChannels: slackConfig.accessRequestChannels,
isSecretRequestNotificationEnabled: slackConfig.isSecretRequestNotificationEnabled,
secretRequestChannels: slackConfig.secretRequestChannels
id: workflowIntegrationConfig.id,
integrationId: workflowIntegrationConfig.integrationId,
integration: workflowIntegrationConfig.integration,
isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled,
accessRequestChannels: workflowIntegrationConfig.accessRequestChannels,
isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled,
secretRequestChannels: workflowIntegrationConfig.secretRequestChannels
}
}
});
return slackConfig;
return workflowIntegrationConfig;
}
});

View File

@@ -7,7 +7,8 @@ const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({
id: true,
description: true,
slug: true,
integration: true
integration: true,
status: true
});
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {

View File

@@ -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"
});

View File

@@ -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
};
};

View File

@@ -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
};
};

View File

@@ -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;
};

View File

@@ -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 };
};

View File

@@ -43,6 +43,9 @@ import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
@@ -60,9 +63,11 @@ import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
import { validateSlackChannelsField } from "../slack/slack-auth-validators";
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue";
@@ -70,10 +75,11 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import {
TCreateProjectDTO,
TDeleteProjectDTO,
TDeleteProjectWorkflowIntegration,
TGetProjectDTO,
TGetProjectKmsKey,
TGetProjectSlackConfig,
TGetProjectSshConfig,
TGetProjectWorkflowIntegrationConfig,
TListProjectAlertsDTO,
TListProjectCasDTO,
TListProjectCertificateTemplatesDTO,
@@ -92,9 +98,9 @@ import {
TUpdateProjectDTO,
TUpdateProjectKmsDTO,
TUpdateProjectNameDTO,
TUpdateProjectSlackConfig,
TUpdateProjectSshConfig,
TUpdateProjectVersionLimitDTO,
TUpdateProjectWorkflowIntegration,
TUpgradeProjectDTO
} from "./project-types";
@@ -123,8 +129,19 @@ type TProjectServiceFactoryDep = {
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
>;
groupProjectDAL: Pick<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">;
microsoftTeamsIntegrationDAL: Pick<
TMicrosoftTeamsIntegrationDALFactory,
"findById" | "findByIdWithWorkflowIntegrationDetails"
>;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
@@ -197,7 +214,9 @@ export const projectServiceFactory = ({
kmsService,
projectBotDAL,
projectSlackConfigDAL,
projectMicrosoftTeamsConfigDAL,
slackIntegrationDAL,
microsoftTeamsIntegrationDAL,
projectTemplateService,
groupProjectDAL,
smtpService
@@ -1452,13 +1471,14 @@ export const projectServiceFactory = ({
return projectSshConfig;
};
const getProjectSlackConfig = async ({
const getProjectWorkflowIntegrationConfig = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId
}: TGetProjectSlackConfig) => {
projectId,
integration
}: TGetProjectWorkflowIntegrationConfig) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
@@ -1477,23 +1497,60 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
return projectSlackConfigDAL.findOne({
projectId: project.id
if (integration === WorkflowIntegration.SLACK) {
const config = await projectSlackConfigDAL.findOne({
projectId: project.id
});
if (!config) {
throw new NotFoundError({
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
});
}
return {
...config,
integration,
integrationId: config.slackIntegrationId
};
}
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
const config = await projectMicrosoftTeamsConfigDAL.findOne({
projectId: project.id
});
if (!config) {
throw new NotFoundError({
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
});
}
return {
...config,
integration,
integrationId: config.microsoftTeamsIntegrationId
};
}
throw new BadRequestError({
message: `Integration type '${integration as string}' not supported`
});
};
const updateProjectSlackConfig = async ({
const updateProjectWorkflowIntegration = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
slackIntegrationId,
integration,
integrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
}: TUpdateProjectSlackConfig) => {
}: TUpdateProjectWorkflowIntegration) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
@@ -1501,17 +1558,206 @@ export const projectServiceFactory = ({
});
}
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
if (!slackIntegration) {
throw new NotFoundError({
message: `Slack integration with ID '${slackIntegrationId}' not found`
if (integration === WorkflowIntegration.SLACK) {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
if (!slackIntegration) {
throw new NotFoundError({
message: `Slack integration with ID '${integrationId}' not found`
});
}
if (slackIntegration.orgId !== actorOrgId) {
throw new ForbiddenRequestError({
message: "Selected slack integration is not in the same organization"
});
}
if (slackIntegration.orgId !== project.orgId) {
throw new ForbiddenRequestError({
message: "Selected slack integration is not in the same organization"
});
}
const updatedWorkflowIntegration = await projectSlackConfigDAL.transaction(async (tx) => {
const slackConfig = await projectSlackConfigDAL.findOne(
{
projectId
},
tx
);
if (slackConfig) {
return projectSlackConfigDAL.updateById(
slackConfig.id,
{
slackIntegrationId: integrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels: sanitizedAccessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels: sanitizedSecretRequestChannels
},
tx
);
}
return projectSlackConfigDAL.create(
{
projectId,
slackIntegrationId: integrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels: sanitizedAccessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels: sanitizedSecretRequestChannels
},
tx
);
});
return {
...updatedWorkflowIntegration,
accessRequestChannels: sanitizedAccessRequestChannels,
secretRequestChannels: sanitizedSecretRequestChannels,
integrationId: slackIntegration.id,
integration: WorkflowIntegration.SLACK
} as const;
}
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (isAccessRequestNotificationEnabled && !accessRequestChannels) {
throw new BadRequestError({
message: "Access request channels are required when access request notifications are enabled"
});
}
if (isSecretRequestNotificationEnabled && !secretRequestChannels) {
throw new BadRequestError({
message: "Secret request channels are required when secret request notifications are enabled"
});
}
if (!secretRequestChannels && !accessRequestChannels) {
throw new BadRequestError({
message: "At least one of access request channels or secret request channels is required"
});
}
const microsoftTeamsIntegration =
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
if (!microsoftTeamsIntegration) {
throw new NotFoundError({
message: `Microsoft Teams integration with ID '${integrationId}' not found`
});
}
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
throw new BadRequestError({
message: "Microsoft Teams integration is not properly installed in your tenant."
});
}
if (microsoftTeamsIntegration.orgId !== actorOrgId) {
throw new ForbiddenRequestError({
message: "Selected Microsoft Teams integration is not in the same organization"
});
}
if (microsoftTeamsIntegration.orgId !== project.orgId) {
throw new ForbiddenRequestError({
message: "Selected Microsoft Teams integration is not in the same organization"
});
}
const sanitizedAccessRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
const sanitizedSecretRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
const updatedWorkflowIntegration = await projectMicrosoftTeamsConfigDAL.transaction(async (tx) => {
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.findOne(
{
projectId
},
tx
);
if (microsoftTeamsConfig) {
return projectMicrosoftTeamsConfigDAL.updateById(
microsoftTeamsConfig.id,
{
microsoftTeamsIntegrationId: integrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels: sanitizedAccessRequestChannels || {},
isSecretRequestNotificationEnabled,
secretRequestChannels: sanitizedSecretRequestChannels || {}
},
tx
);
}
return projectMicrosoftTeamsConfigDAL.create(
{
projectId,
microsoftTeamsIntegrationId: integrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels: sanitizedAccessRequestChannels || {},
isSecretRequestNotificationEnabled,
secretRequestChannels: sanitizedSecretRequestChannels || {}
},
tx
);
});
return {
...updatedWorkflowIntegration,
accessRequestChannels: sanitizedAccessRequestChannels,
secretRequestChannels: sanitizedSecretRequestChannels,
integrationId: microsoftTeamsIntegration.id,
integration: WorkflowIntegration.MICROSOFT_TEAMS
} as const;
}
if (slackIntegration.orgId !== actorOrgId) {
throw new ForbiddenRequestError({
message: "Selected slack integration is not in the same organization"
throw new BadRequestError({
message: `Integration type '${integration as string}' not supported`
});
};
const deleteProjectWorkflowIntegration = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
integrationId,
integration
}: TDeleteProjectWorkflowIntegration) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: `Project with ID '${projectId}' not found`
});
}
@@ -1524,47 +1770,28 @@ export const projectServiceFactory = ({
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
if (slackIntegration.orgId !== project.orgId) {
throw new ForbiddenRequestError({
message: "Selected slack integration is not in the same organization"
if (integration === WorkflowIntegration.SLACK) {
const [deletedIntegration] = await projectSlackConfigDAL.delete({
projectId,
slackIntegrationId: integrationId
});
return deletedIntegration;
}
return projectSlackConfigDAL.transaction(async (tx) => {
const slackConfig = await projectSlackConfigDAL.findOne(
{
projectId
},
tx
);
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({
projectId,
microsoftTeamsIntegrationId: integrationId
});
if (slackConfig) {
return projectSlackConfigDAL.updateById(
slackConfig.id,
{
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
}
return deletedIntegration;
}
return projectSlackConfigDAL.create(
{
projectId,
slackIntegrationId,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
},
tx
);
throw new BadRequestError({
message: `Integration with ID '${integrationId}' not found`
});
};
@@ -1673,10 +1900,11 @@ export const projectServiceFactory = ({
getProjectKmsBackup,
loadProjectKmsBackup,
getProjectKmsKeys,
getProjectWorkflowIntegrationConfig,
updateProjectWorkflowIntegration,
deleteProjectWorkflowIntegration,
getProjectSshConfig,
updateProjectSshConfig,
getProjectSlackConfig,
updateProjectSlackConfig,
requestProjectAccess,
searchProjects
};

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 { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
enum KmsType {
External = "external",
@@ -166,14 +167,33 @@ export type TUpdateProjectSshConfig = {
export type TGetProjectSshConfig = TProjectPermission;
export type TGetProjectSlackConfig = TProjectPermission;
export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & {
integration: WorkflowIntegration;
};
export type TUpdateProjectSlackConfig = {
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
export type TUpdateProjectWorkflowIntegration = (
| {
integrationId: string;
integration: WorkflowIntegration.SLACK;
isAccessRequestNotificationEnabled: boolean;
isSecretRequestNotificationEnabled: boolean;
accessRequestChannels?: string;
secretRequestChannels?: string;
}
| {
integrationId: string;
integration: WorkflowIntegration.MICROSOFT_TEAMS;
isAccessRequestNotificationEnabled: boolean;
isSecretRequestNotificationEnabled: boolean;
accessRequestChannels?: { teamId: string; channelIds: string[] };
secretRequestChannels?: { teamId: string; channelIds: string[] };
}
) &
TProjectPermission;
export type TDeleteProjectWorkflowIntegration = {
integrationId: string;
integration: WorkflowIntegration;
} & TProjectPermission;
export type TBootstrapSshProjectDTO = {

View File

@@ -3,12 +3,10 @@ import { WebClient } from "@slack/web-api";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
import { TSendSlackNotificationDTO } from "./slack-types";
export const fetchSlackChannels = async (botKey: string) => {
const slackChannels: {
@@ -41,11 +39,11 @@ export const fetchSlackChannels = async (botKey: string) => {
return slackChannels;
};
const buildSlackPayload = (notification: TSlackNotification) => {
const buildSlackPayload = (notification: TNotification) => {
const appCfg = getConfig();
switch (notification.type) {
case SlackTriggerFeature.SECRET_APPROVAL: {
case TriggerFeature.SECRET_APPROVAL: {
const { payload } = notification;
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
*Environment*: ${payload.environment}
@@ -79,7 +77,7 @@ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId
payloadBlocks
};
}
case SlackTriggerFeature.ACCESS_REQUEST: {
case TriggerFeature.ACCESS_REQUEST: {
const { payload } = notification;
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
payload.isTemporary ? "temporary" : "permanent"
@@ -125,51 +123,24 @@ User Note: ${payload.note}`
}
};
export const triggerSlackNotification = async ({
projectId,
export const sendSlackNotification = async ({
orgId,
notification,
projectSlackConfigDAL,
projectDAL,
kmsService
}: {
projectId: string;
notification: TSlackNotification;
projectSlackConfigDAL: Pick<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;
}
}
kmsService,
targetChannelIds,
slackIntegration
}: TSendSlackNotificationDTO) => {
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: project.orgId
orgId
});
const botKey = orgDataKeyDecryptor({
cipherTextBlob: slackIntegration.encryptedBotAccessToken
}).toString("utf8");
const slackWebClient = new WebClient(botKey);
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
for await (const conversationId of targetChannelIds) {
// we send both text and blocks for compatibility with barebone clients
await slackWebClient.chat

View File

@@ -1,4 +1,8 @@
import { TSlackIntegrations } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types";
import { TNotification } from "@app/lib/workflow-integrations/types";
import { TKmsServiceFactory } from "../kms/kms-service";
export type TGetSlackInstallUrlDTO = {
slug: string;
@@ -48,34 +52,10 @@ export type TReinstallSlackIntegrationDTO = {
slackBotUserId: string;
};
export enum SlackTriggerFeature {
SECRET_APPROVAL = "secret-approval",
ACCESS_REQUEST = "access-request"
}
export type TSlackNotification =
| {
type: SlackTriggerFeature.SECRET_APPROVAL;
payload: {
userEmail: string;
environment: string;
secretPath: string;
requestId: string;
projectId: string;
secretKeys: string[];
};
}
| {
type: SlackTriggerFeature.ACCESS_REQUEST;
payload: {
requesterFullName: string;
requesterEmail: string;
isTemporary: boolean;
secretPath: string;
environment: string;
projectName: string;
permissions: string[];
approvalUrl: string;
note?: string;
};
};
export type TSendSlackNotificationDTO = {
orgId: string;
notification: TNotification;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
targetChannelIds: string[];
slackIntegration: TSlackIntegrations;
};

View File

@@ -20,6 +20,7 @@ import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { RootKeyEncryptionStrategy } from "../kms/kms-types";
import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service";
import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
@@ -47,6 +48,7 @@ type TSuperAdminServiceFactoryDep = {
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
};
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
@@ -77,7 +79,8 @@ export const superAdminServiceFactory = ({
licenseService,
identityAccessTokenDAL,
identityTokenAuthDAL,
identityOrgMembershipDAL
identityOrgMembershipDAL,
microsoftTeamsService
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself
@@ -125,7 +128,13 @@ export const superAdminServiceFactory = ({
};
const updateServerCfg = async (
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string },
data: TSuperAdminUpdate & {
slackClientId?: string;
slackClientSecret?: string;
microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string;
},
userId: string
) => {
const updatedData = data;
@@ -192,10 +201,51 @@ export const superAdminServiceFactory = ({
updatedData.slackClientSecret = undefined;
}
let microsoftTeamsSettingsUpdated = false;
if (data.microsoftTeamsAppId) {
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));
updatedData.encryptedMicrosoftTeamsAppId = encryptedClientId;
updatedData.microsoftTeamsAppId = undefined;
microsoftTeamsSettingsUpdated = true;
}
if (data.microsoftTeamsClientSecret) {
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.microsoftTeamsClientSecret));
updatedData.encryptedMicrosoftTeamsClientSecret = encryptedClientSecret;
updatedData.microsoftTeamsClientSecret = undefined;
microsoftTeamsSettingsUpdated = true;
}
if (data.microsoftTeamsBotId) {
const encryptedBotId = encryptWithRoot(Buffer.from(data.microsoftTeamsBotId));
updatedData.encryptedMicrosoftTeamsBotId = encryptedBotId;
updatedData.microsoftTeamsBotId = undefined;
microsoftTeamsSettingsUpdated = true;
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
if (
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
updatedServerCfg.encryptedMicrosoftTeamsBotId &&
microsoftTeamsSettingsUpdated
) {
const decryptWithRoot = kmsService.decryptWithRootKey();
decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsBotId); // validate that we're able to decrypt the bot ID
const decryptedAppId = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsAppId);
const decryptedAppPassword = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsClientSecret);
await microsoftTeamsService.initializeTeamsBot({
botAppId: decryptedAppId.toString(),
botAppPassword: decryptedAppPassword.toString()
});
}
return updatedServerCfg;
};
@@ -488,29 +538,40 @@ export const superAdminServiceFactory = ({
await userDAL.updateById(userId, { superAdmin: true });
};
const getAdminSlackConfig = async () => {
const getAdminIntegrationsConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg) {
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
}
let clientId = "";
let clientSecret = "";
const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) {
clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
}
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
const slackClientSecret = serverCfg.encryptedSlackClientSecret
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
: "";
if (serverCfg.encryptedSlackClientSecret) {
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
}
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
: "";
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
: "";
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
: "";
return {
clientId,
clientSecret
slack: {
clientSecret: slackClientSecret,
clientId: slackClientId
},
microsoftTeams: {
appId: microsoftAppId,
clientSecret: microsoftClientSecret,
botId: microsoftBotId
}
};
};
@@ -578,7 +639,7 @@ export const superAdminServiceFactory = ({
getUsers,
deleteUser,
getIdentities,
getAdminSlackConfig,
getAdminIntegrationsConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser,

View File

@@ -1,7 +1,13 @@
import { TOrgPermission } from "@app/lib/types";
export enum WorkflowIntegration {
SLACK = "slack"
SLACK = "slack",
MICROSOFT_TEAMS = "microsoft-teams"
}
export enum WorkflowIntegrationStatus {
PENDING = "pending",
INSTALLED = "installed"
}
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;