Fix merge conflicts
@@ -25,3 +25,6 @@ frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationL
|
||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
||||
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:85
|
||||
docs/cli/commands/user.mdx:generic-api-key:51
|
||||
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
|
||||
docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -101,6 +101,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
@@ -248,6 +249,7 @@ declare module "fastify" {
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
};
|
||||
|
||||
@@ -432,6 +432,16 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TMicrosoftTeamsIntegrations,
|
||||
TMicrosoftTeamsIntegrationsInsert,
|
||||
TMicrosoftTeamsIntegrationsUpdate
|
||||
} from "@app/db/schemas/microsoft-teams-integrations";
|
||||
import {
|
||||
TProjectMicrosoftTeamsConfigs,
|
||||
TProjectMicrosoftTeamsConfigsInsert,
|
||||
TProjectMicrosoftTeamsConfigsUpdate
|
||||
} from "@app/db/schemas/project-microsoft-teams-configs";
|
||||
import {
|
||||
TSecretReminderRecipients,
|
||||
TSecretReminderRecipientsInsert,
|
||||
@@ -1018,6 +1028,16 @@ declare module "knex/types/tables" {
|
||||
TSecretRotationV2SecretMappingsInsert,
|
||||
TSecretRotationV2SecretMappingsUpdate
|
||||
>;
|
||||
[TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType<
|
||||
TMicrosoftTeamsIntegrations,
|
||||
TMicrosoftTeamsIntegrationsInsert,
|
||||
TMicrosoftTeamsIntegrationsUpdate
|
||||
>;
|
||||
[TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType<
|
||||
TProjectMicrosoftTeamsConfigs,
|
||||
TProjectMicrosoftTeamsConfigsInsert,
|
||||
TProjectMicrosoftTeamsConfigsUpdate
|
||||
>;
|
||||
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
|
||||
TSecretReminderRecipients,
|
||||
TSecretReminderRecipientsInsert,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
>;
|
||||
@@ -149,6 +149,8 @@ export enum TableName {
|
||||
KmipClientCertificates = "kmip_client_certificates",
|
||||
SecretRotationV2 = "secret_rotations_v2",
|
||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
|
||||
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
|
||||
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
|
||||
SecretReminderRecipients = "secret_reminder_recipients",
|
||||
GithubOrgSyncConfig = "github_org_sync_configs"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
>;
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
TSecretSyncRaw,
|
||||
TUpdateSecretSyncDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||
|
||||
import { KmipPermission } from "../kmip/kmip-enum";
|
||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||
@@ -252,11 +253,14 @@ export enum EventType {
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_SLACK_INTEGRATION = "get-slack-integration",
|
||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
|
||||
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
|
||||
|
||||
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
||||
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
||||
INTEGRATION_SYNCED = "integration-synced",
|
||||
@@ -329,6 +333,15 @@ export enum EventType {
|
||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||
|
||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||
}
|
||||
@@ -2038,22 +2051,24 @@ interface GetSlackIntegration {
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectSlackConfig {
|
||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
||||
interface UpdateProjectWorkflowIntegrationConfig {
|
||||
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
slackIntegrationId: string;
|
||||
integrationId: string;
|
||||
integration: WorkflowIntegration;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectSlackConfig {
|
||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
||||
interface GetProjectWorkflowIntegrationConfig {
|
||||
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
integration: WorkflowIntegration;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2619,6 +2634,66 @@ interface RotateSecretRotationEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationCreateEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationDeleteEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationGetEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationListEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST;
|
||||
metadata: Record<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
|
||||
@@ -2781,8 +2856,8 @@ export type Event =
|
||||
| UpdateSlackIntegration
|
||||
| DeleteSlackIntegration
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| UpdateProjectWorkflowIntegrationConfig
|
||||
| GetProjectWorkflowIntegrationConfig
|
||||
| GetProjectSshConfig
|
||||
| UpdateProjectSshConfig
|
||||
| IntegrationSyncedEvent
|
||||
@@ -2859,4 +2934,11 @@ export type Event =
|
||||
| CreateSecretRotationEvent
|
||||
| UpdateSecretRotationEvent
|
||||
| DeleteSecretRotationEvent
|
||||
| RotateSecretRotationEvent;
|
||||
| RotateSecretRotationEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationCreateEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationDeleteEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -1901,6 +1901,13 @@ export const AppConnections = {
|
||||
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
|
||||
accessToken: "The access token to use to connect with Windmill."
|
||||
},
|
||||
HC_VAULT: {
|
||||
instanceUrl: "The Hashicrop Vault instance URL to connect with.",
|
||||
namespace: "The Hashicrop Vault namespace to connect with.",
|
||||
accessToken: "The access token used to connect with Hashicorp Vault.",
|
||||
roleId: "The Role ID used to connect with Hashicorp Vault.",
|
||||
secretId: "The Secret ID used to connect with Hashicorp Vault."
|
||||
},
|
||||
LDAP: {
|
||||
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
|
||||
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
|
||||
@@ -2058,6 +2065,10 @@ export const SecretSyncs = {
|
||||
workspace: "The Windmill workspace to sync secrets to.",
|
||||
path: "The Windmill workspace path to sync secrets to."
|
||||
},
|
||||
HC_VAULT: {
|
||||
mount: "The Hashicorp Vault Secrets Engine Mount to sync secrets to.",
|
||||
path: "The Hashicorp Vault path to sync secrets to."
|
||||
},
|
||||
TEAMCITY: {
|
||||
project: "The TeamCity project to sync secrets to.",
|
||||
buildConfig: "The TeamCity build configuration to sync secrets to."
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||
import { sendSlackNotification } from "@app/services/slack/slack-fns";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types";
|
||||
|
||||
export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => {
|
||||
try {
|
||||
const { projectId, notification } = dto.input;
|
||||
const { projectDAL, projectSlackConfigDAL, kmsService, projectMicrosoftTeamsConfigDAL, microsoftTeamsService } =
|
||||
dto.dependencies;
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||
const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||
|
||||
if (slackConfig) {
|
||||
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||
const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || [];
|
||||
if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) {
|
||||
await sendSlackNotification({
|
||||
orgId: project.orgId,
|
||||
notification,
|
||||
kmsService,
|
||||
targetChannelIds,
|
||||
slackIntegration: slackConfig
|
||||
}).catch((error) => {
|
||||
logger.error(error, "Error sending Slack notification");
|
||||
});
|
||||
}
|
||||
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||
const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || [];
|
||||
if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) {
|
||||
await sendSlackNotification({
|
||||
orgId: project.orgId,
|
||||
notification,
|
||||
kmsService,
|
||||
targetChannelIds,
|
||||
slackIntegration: slackConfig
|
||||
}).catch((error) => {
|
||||
logger.error(error, "Error sending Slack notification");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (microsoftTeamsConfig) {
|
||||
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
|
||||
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
||||
microsoftTeamsConfig.accessRequestChannels
|
||||
);
|
||||
|
||||
if (success && data) {
|
||||
await microsoftTeamsService
|
||||
.sendNotification({
|
||||
notification,
|
||||
target: data,
|
||||
tenantId: microsoftTeamsConfig.tenantId,
|
||||
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||
orgId: project.orgId
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error, "Error sending Microsoft Teams notification");
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||
if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) {
|
||||
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
||||
microsoftTeamsConfig.secretRequestChannels
|
||||
);
|
||||
|
||||
if (success && data) {
|
||||
await microsoftTeamsService
|
||||
.sendNotification({
|
||||
notification,
|
||||
target: data,
|
||||
tenantId: microsoftTeamsConfig.tenantId,
|
||||
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||
orgId: project.orgId
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error, "Error sending Microsoft Teams notification");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, "Error triggering workflow integration notification");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
|
||||
export enum TriggerFeature {
|
||||
SECRET_APPROVAL = "secret-approval",
|
||||
ACCESS_REQUEST = "access-request"
|
||||
}
|
||||
|
||||
export type TNotification =
|
||||
| {
|
||||
type: TriggerFeature.SECRET_APPROVAL;
|
||||
payload: {
|
||||
userEmail: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
secretKeys: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: TriggerFeature.ACCESS_REQUEST;
|
||||
payload: {
|
||||
requesterFullName: string;
|
||||
requesterEmail: string;
|
||||
isTemporary: boolean;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectName: string;
|
||||
permissions: string[];
|
||||
approvalUrl: string;
|
||||
note?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TTriggerWorkflowNotificationDTO = {
|
||||
input: {
|
||||
projectId: string;
|
||||
notification: TNotification;
|
||||
};
|
||||
dependencies: {
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
};
|
||||
};
|
||||
@@ -111,6 +111,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -177,6 +177,9 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal";
|
||||
import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||
@@ -431,6 +434,8 @@ export const registerRoutes = async (
|
||||
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
|
||||
|
||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
|
||||
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
@@ -628,6 +633,7 @@ export const registerRoutes = async (
|
||||
tokenService,
|
||||
orgDAL,
|
||||
totpService,
|
||||
orgMembershipDAL,
|
||||
auditLogService
|
||||
});
|
||||
const passwordService = authPaswordServiceFactory({
|
||||
@@ -692,6 +698,15 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
const microsoftTeamsService = microsoftTeamsServiceFactory({
|
||||
microsoftTeamsIntegrationDAL,
|
||||
permissionService,
|
||||
workflowIntegrationDAL,
|
||||
kmsService,
|
||||
serverCfgDAL: superAdminDAL
|
||||
});
|
||||
|
||||
const superAdminService = superAdminServiceFactory({
|
||||
userDAL,
|
||||
identityDAL,
|
||||
@@ -705,7 +720,8 @@ export const registerRoutes = async (
|
||||
orgService,
|
||||
keyStore,
|
||||
licenseService,
|
||||
kmsService
|
||||
kmsService,
|
||||
microsoftTeamsService
|
||||
});
|
||||
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
@@ -1044,6 +1060,8 @@ export const registerRoutes = async (
|
||||
certificateTemplateDAL,
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService
|
||||
@@ -1168,7 +1186,9 @@ export const registerRoutes = async (
|
||||
userDAL,
|
||||
licenseService,
|
||||
projectSlackConfigDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
});
|
||||
|
||||
const secretService = secretServiceFactory({
|
||||
@@ -1230,7 +1250,9 @@ export const registerRoutes = async (
|
||||
accessApprovalPolicyApproverDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
groupDAL
|
||||
groupDAL,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
});
|
||||
|
||||
const secretReplicationService = secretReplicationServiceFactory({
|
||||
@@ -1628,6 +1650,7 @@ export const registerRoutes = async (
|
||||
await dailyResourceCleanUp.startCleanUp();
|
||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||
await kmsService.startService();
|
||||
await microsoftTeamsService.start();
|
||||
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -1721,6 +1744,7 @@ export const registerRoutes = async (
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService,
|
||||
secretRotationV2: secretRotationV2Service,
|
||||
microsoftTeams: microsoftTeamsService,
|
||||
assumePrivileges: assumePrivilegeService,
|
||||
githubOrgSync: githubOrgSyncConfigService
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
import {
|
||||
HCVaultConnectionListItemSchema,
|
||||
SanitizedHCVaultConnectionSchema
|
||||
} from "@app/services/app-connection/hc-vault";
|
||||
import {
|
||||
HumanitecConnectionListItemSchema,
|
||||
SanitizedHumanitecConnectionSchema
|
||||
@@ -68,6 +72,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedMsSqlConnectionSchema.options,
|
||||
...SanitizedCamundaConnectionSchema.options,
|
||||
...SanitizedAuth0ConnectionSchema.options,
|
||||
...SanitizedHCVaultConnectionSchema.options,
|
||||
...SanitizedAzureClientSecretsConnectionSchema.options,
|
||||
...SanitizedWindmillConnectionSchema.options,
|
||||
...SanitizedLdapConnectionSchema.options,
|
||||
@@ -88,6 +93,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
MsSqlConnectionListItemSchema,
|
||||
CamundaConnectionListItemSchema,
|
||||
Auth0ConnectionListItemSchema,
|
||||
HCVaultConnectionListItemSchema,
|
||||
AzureClientSecretsConnectionListItemSchema,
|
||||
WindmillConnectionListItemSchema,
|
||||
LdapConnectionListItemSchema,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateHCVaultConnectionSchema,
|
||||
SanitizedHCVaultConnectionSchema,
|
||||
UpdateHCVaultConnectionSchema
|
||||
} from "@app/services/app-connection/hc-vault";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerHCVaultConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.HCVault,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedHCVaultConnectionSchema,
|
||||
createSchema: CreateHCVaultConnectionSchema,
|
||||
updateSchema: UpdateHCVaultConnectionSchema
|
||||
});
|
||||
|
||||
// The following endpoints are for internal Infisical App use only and not part of the public API
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/mounts`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.string().array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const mounts = await server.services.appConnection.hcvault.listMounts(connectionId, req.permission);
|
||||
return mounts;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
@@ -37,6 +38,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
||||
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
|
||||
[AppConnection.HCVault]: registerHCVaultConnectionRouter,
|
||||
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
||||
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
|
||||
};
|
||||
|
||||
@@ -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" }
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreateHCVaultSyncSchema,
|
||||
HCVaultSyncSchema,
|
||||
UpdateHCVaultSyncSchema
|
||||
} from "@app/services/secret-sync/hc-vault";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerHCVaultSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.HCVault,
|
||||
server,
|
||||
responseSchema: HCVaultSyncSchema,
|
||||
createSchema: CreateHCVaultSyncSchema,
|
||||
updateSchema: UpdateHCVaultSyncSchema
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
|
||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||
@@ -29,5 +30,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||
[SecretSync.Vercel]: registerVercelSyncRouter,
|
||||
[SecretSync.Windmill]: registerWindmillSyncRouter,
|
||||
[SecretSync.HCVault]: registerHCVaultSyncRouter,
|
||||
[SecretSync.TeamCity]: registerTeamCitySyncRouter
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secr
|
||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
@@ -41,6 +42,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
CamundaSyncSchema,
|
||||
VercelSyncSchema,
|
||||
WindmillSyncSchema,
|
||||
HCVaultSyncSchema,
|
||||
TeamCitySyncSchema
|
||||
]);
|
||||
|
||||
@@ -57,6 +59,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
CamundaSyncListItemSchema,
|
||||
VercelSyncListItemSchema,
|
||||
WindmillSyncListItemSchema,
|
||||
HCVaultSyncListItemSchema,
|
||||
TeamCitySyncListItemSchema
|
||||
]);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||
import { OrgAuthMethod } from "@app/services/org/org-types";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -342,8 +343,12 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const serverCfg = await getServerCfg();
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
|
||||
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
|
||||
serverCfg.defaultAuthOrgId && !appCfg.isCloud ? `&defaultOrgAllowed=true` : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -88,24 +88,41 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
email: z.string().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional(),
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
publicKey: z.string().trim(),
|
||||
encryptedPrivateKey: z.string().trim(),
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim(),
|
||||
organizationName: GenericResourceNameSchema,
|
||||
providerAuthToken: z.string().trim().optional().nullish(),
|
||||
attributionSource: z.string().trim().optional(),
|
||||
password: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
email: z.string().trim(),
|
||||
firstName: z.string().trim(),
|
||||
lastName: z.string().trim().optional(),
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
publicKey: z.string().trim(),
|
||||
encryptedPrivateKey: z.string().trim(),
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim(),
|
||||
providerAuthToken: z.string().trim().optional().nullish(),
|
||||
attributionSource: z.string().trim().optional(),
|
||||
password: z.string()
|
||||
})
|
||||
.and(
|
||||
z.preprocess(
|
||||
(data) => {
|
||||
if (typeof data === "object" && data && "useDefaultOrg" in data === false) {
|
||||
return { ...data, useDefaultOrg: false };
|
||||
}
|
||||
return data;
|
||||
},
|
||||
z.discriminatedUnion("useDefaultOrg", [
|
||||
z.object({ useDefaultOrg: z.literal(true) }),
|
||||
z.object({
|
||||
useDefaultOrg: z.literal(false),
|
||||
organizationName: GenericResourceNameSchema
|
||||
})
|
||||
])
|
||||
)
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
|
||||
@@ -14,6 +14,7 @@ export enum AppConnection {
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0",
|
||||
HCVault = "hashicorp-vault",
|
||||
LDAP = "ldap",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ import {
|
||||
} from "./databricks";
|
||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||
import {
|
||||
getHCVaultConnectionListItem,
|
||||
HCVaultConnectionMethod,
|
||||
validateHCVaultConnectionCredentials
|
||||
} from "./hc-vault";
|
||||
import {
|
||||
getHumanitecConnectionListItem,
|
||||
HumanitecConnectionMethod,
|
||||
@@ -84,6 +89,7 @@ export const listAppConnectionOptions = () => {
|
||||
getAzureClientSecretsConnectionListItem(),
|
||||
getWindmillConnectionListItem(),
|
||||
getAuth0ConnectionListItem(),
|
||||
getHCVaultConnectionListItem(),
|
||||
getLdapConnectionListItem(),
|
||||
getTeamCityConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -152,6 +158,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
@@ -186,10 +193,13 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
case HCVaultConnectionMethod.AppRole:
|
||||
return "App Role";
|
||||
case LdapConnectionMethod.SimpleBind:
|
||||
return "Simple Bind";
|
||||
default:
|
||||
@@ -238,6 +248,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.HCVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Windmill]: "Windmill",
|
||||
[AppConnection.Auth0]: "Auth0",
|
||||
[AppConnection.HCVault]: "Hashicorp Vault",
|
||||
[AppConnection.LDAP]: "LDAP",
|
||||
[AppConnection.TeamCity]: "TeamCity"
|
||||
};
|
||||
|
||||
@@ -43,6 +43,8 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
|
||||
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
|
||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
@@ -81,6 +83,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema,
|
||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
|
||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
||||
};
|
||||
@@ -459,6 +462,7 @@ export const appConnectionServiceFactory = ({
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||
};
|
||||
|
||||
@@ -57,6 +57,12 @@ import {
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentialsSchema
|
||||
} from "./github";
|
||||
import {
|
||||
THCVaultConnection,
|
||||
THCVaultConnectionConfig,
|
||||
THCVaultConnectionInput,
|
||||
TValidateHCVaultConnectionCredentialsSchema
|
||||
} from "./hc-vault";
|
||||
import {
|
||||
THumanitecConnection,
|
||||
THumanitecConnectionConfig,
|
||||
@@ -116,6 +122,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TAzureClientSecretsConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
| THCVaultConnection
|
||||
| TLdapConnection
|
||||
| TTeamCityConnection
|
||||
);
|
||||
@@ -140,6 +147,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TAzureClientSecretsConnectionInput
|
||||
| TWindmillConnectionInput
|
||||
| TAuth0ConnectionInput
|
||||
| THCVaultConnectionInput
|
||||
| TLdapConnectionInput
|
||||
| TTeamCityConnectionInput
|
||||
);
|
||||
@@ -170,6 +178,7 @@ export type TAppConnectionConfig =
|
||||
| TVercelConnectionConfig
|
||||
| TWindmillConnectionConfig
|
||||
| TAuth0ConnectionConfig
|
||||
| THCVaultConnectionConfig
|
||||
| TLdapConnectionConfig
|
||||
| TTeamCityConnectionConfig;
|
||||
|
||||
@@ -189,6 +198,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||
| TValidateWindmillConnectionCredentialsSchema
|
||||
| TValidateAuth0ConnectionCredentialsSchema
|
||||
| TValidateHCVaultConnectionCredentialsSchema
|
||||
| TValidateLdapConnectionCredentialsSchema
|
||||
| TValidateTeamCityConnectionCredentialsSchema;
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum HCVaultConnectionMethod {
|
||||
AccessToken = "access-token",
|
||||
AppRole = "app-role"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
|
||||
import {
|
||||
THCVaultConnection,
|
||||
THCVaultConnectionConfig,
|
||||
THCVaultMountResponse,
|
||||
TValidateHCVaultConnectionCredentials
|
||||
} from "./hc-vault-connection-types";
|
||||
|
||||
export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => {
|
||||
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
return instanceUrl;
|
||||
};
|
||||
|
||||
export const getHCVaultConnectionListItem = () => ({
|
||||
name: "HCVault" as const,
|
||||
app: AppConnection.HCVault as const,
|
||||
methods: Object.values(HCVaultConnectionMethod) as [
|
||||
HCVaultConnectionMethod.AccessToken,
|
||||
HCVaultConnectionMethod.AppRole
|
||||
]
|
||||
});
|
||||
|
||||
type TokenRespData = {
|
||||
auth: {
|
||||
client_token: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnectionCredentials) => {
|
||||
// Return access token directly if not using AppRole method
|
||||
if (connection.method !== HCVaultConnectionMethod.AppRole) {
|
||||
return connection.credentials.accessToken;
|
||||
}
|
||||
|
||||
// Generate temporary token for AppRole method
|
||||
try {
|
||||
const { instanceUrl, roleId, secretId } = connection.credentials;
|
||||
const tokenResp = await request.post<TokenRespData>(
|
||||
`${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`,
|
||||
{ role_id: roleId, secret_id: secretId },
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {})
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (tokenResp.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate credentials: Hashicorp Vault responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
|
||||
});
|
||||
}
|
||||
|
||||
return tokenResp.data.auth.client_token;
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateHCVaultConnectionCredentials = async (config: THCVaultConnectionConfig) => {
|
||||
const instanceUrl = await getHCVaultInstanceUrl(config);
|
||||
|
||||
try {
|
||||
const accessToken = await getHCVaultAccessToken(config);
|
||||
|
||||
// Verify token
|
||||
await request.get(`${instanceUrl}/v1/auth/token/lookup-self`, {
|
||||
headers: { "X-Vault-Token": accessToken }
|
||||
});
|
||||
|
||||
return config.credentials;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listHCVaultMounts = async (appConnection: THCVaultConnection) => {
|
||||
const instanceUrl = await getHCVaultInstanceUrl(appConnection);
|
||||
const accessToken = await getHCVaultAccessToken(appConnection);
|
||||
|
||||
const { data } = await request.get<THCVaultMountResponse>(`${instanceUrl}/v1/sys/mounts`, {
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
...(appConnection.credentials.namespace ? { "X-Vault-Namespace": appConnection.credentials.namespace } : {})
|
||||
}
|
||||
});
|
||||
|
||||
const mounts: string[] = [];
|
||||
|
||||
// Filter for "kv" version 2 type only
|
||||
Object.entries(data.data).forEach(([path, mount]) => {
|
||||
if (mount.type === "kv" && mount.options?.version === "2") {
|
||||
mounts.push(path);
|
||||
}
|
||||
});
|
||||
|
||||
return mounts;
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
|
||||
|
||||
const InstanceUrlSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Instance URL required")
|
||||
.url("Invalid Instance URL")
|
||||
.describe(AppConnections.CREDENTIALS.HC_VAULT.instanceUrl);
|
||||
|
||||
const NamespaceSchema = z.string().trim().optional().describe(AppConnections.CREDENTIALS.HC_VAULT.namespace);
|
||||
|
||||
export const HCVaultConnectionAccessTokenCredentialsSchema = z.object({
|
||||
instanceUrl: InstanceUrlSchema,
|
||||
namespace: NamespaceSchema,
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.describe(AppConnections.CREDENTIALS.HC_VAULT.accessToken)
|
||||
});
|
||||
|
||||
export const HCVaultConnectionAppRoleCredentialsSchema = z.object({
|
||||
instanceUrl: InstanceUrlSchema,
|
||||
namespace: NamespaceSchema,
|
||||
roleId: z.string().trim().min(1, "Role ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.roleId),
|
||||
secretId: z.string().trim().min(1, "Secret ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.secretId)
|
||||
});
|
||||
|
||||
const BaseHCVaultConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.HCVault) });
|
||||
|
||||
export const HCVaultConnectionSchema = z.intersection(
|
||||
BaseHCVaultConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(HCVaultConnectionMethod.AccessToken),
|
||||
credentials: HCVaultConnectionAccessTokenCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(HCVaultConnectionMethod.AppRole),
|
||||
credentials: HCVaultConnectionAppRoleCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseHCVaultConnectionSchema.extend({
|
||||
method: z.literal(HCVaultConnectionMethod.AccessToken),
|
||||
credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({})
|
||||
}),
|
||||
BaseHCVaultConnectionSchema.extend({
|
||||
method: z.literal(HCVaultConnectionMethod.AppRole),
|
||||
credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(HCVaultConnectionMethod.AccessToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.HCVault).method),
|
||||
credentials: HCVaultConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.HCVault).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(HCVaultConnectionMethod.AppRole).describe(AppConnections.CREATE(AppConnection.HCVault).method),
|
||||
credentials: HCVaultConnectionAppRoleCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.HCVault).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateHCVaultConnectionSchema = ValidateHCVaultConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault)
|
||||
);
|
||||
|
||||
export const UpdateHCVaultConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([HCVaultConnectionAccessTokenCredentialsSchema, HCVaultConnectionAppRoleCredentialsSchema])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.HCVault).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault));
|
||||
|
||||
export const HCVaultConnectionListItemSchema = z.object({
|
||||
name: z.literal("HCVault"),
|
||||
app: z.literal(AppConnection.HCVault),
|
||||
methods: z.nativeEnum(HCVaultConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listHCVaultMounts } from "./hc-vault-connection-fns";
|
||||
import { THCVaultConnection } from "./hc-vault-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<THCVaultConnection>;
|
||||
|
||||
export const hcVaultConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listMounts = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor);
|
||||
|
||||
try {
|
||||
const mounts = await listHCVaultMounts(appConnection);
|
||||
return mounts;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with Hashicorp Vault");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listMounts
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateHCVaultConnectionSchema,
|
||||
HCVaultConnectionSchema,
|
||||
ValidateHCVaultConnectionCredentialsSchema
|
||||
} from "./hc-vault-connection-schemas";
|
||||
|
||||
export type THCVaultConnection = z.infer<typeof HCVaultConnectionSchema>;
|
||||
|
||||
export type THCVaultConnectionInput = z.infer<typeof CreateHCVaultConnectionSchema> & {
|
||||
app: AppConnection.HCVault;
|
||||
};
|
||||
|
||||
export type TValidateHCVaultConnectionCredentialsSchema = typeof ValidateHCVaultConnectionCredentialsSchema;
|
||||
|
||||
export type TValidateHCVaultConnectionCredentials = z.infer<typeof ValidateHCVaultConnectionCredentialsSchema>;
|
||||
|
||||
export type THCVaultConnectionConfig = DiscriminativePick<THCVaultConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type THCVaultMountResponse = {
|
||||
data: {
|
||||
[key: string]: {
|
||||
options: {
|
||||
version?: string | null;
|
||||
} | null;
|
||||
type: string; // We're only interested in "kv" types
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./hc-vault-connection-enums";
|
||||
export * from "./hc-vault-connection-fns";
|
||||
export * from "./hc-vault-connection-schemas";
|
||||
export * from "./hc-vault-connection-types";
|
||||
@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
@@ -20,6 +20,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
|
||||
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { LoginMethod } from "../super-admin/super-admin-types";
|
||||
import { TTotpServiceFactory } from "../totp/totp-service";
|
||||
@@ -48,6 +50,7 @@ type TAuthLoginServiceFactoryDep = {
|
||||
smtpService: TSmtpService;
|
||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
orgMembershipDAL: TOrgMembershipDALFactory;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@@ -56,6 +59,7 @@ export const authLoginServiceFactory = ({
|
||||
tokenService,
|
||||
smtpService,
|
||||
orgDAL,
|
||||
orgMembershipDAL,
|
||||
totpService,
|
||||
auditLogService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
@@ -719,6 +723,35 @@ export const authLoginServiceFactory = ({
|
||||
authMethods: [authMethod],
|
||||
isGhost: false
|
||||
});
|
||||
|
||||
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||
let orgId = "";
|
||||
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||
if (!defaultOrg) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find default organization with ID ${serverCfg.defaultAuthOrgId}`
|
||||
});
|
||||
}
|
||||
orgId = defaultOrg.id;
|
||||
const [orgMembership] = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
});
|
||||
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole);
|
||||
|
||||
await orgMembershipDAL.create({
|
||||
userId: user.id,
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role,
|
||||
roleId,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||
if (isLinkingRequired) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { getMinExpiresIn } from "@app/lib/fn";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
@@ -150,7 +150,8 @@ export const authSignupServiceFactory = ({
|
||||
encryptedPrivateKeyTag,
|
||||
ip,
|
||||
userAgent,
|
||||
authorization
|
||||
authorization,
|
||||
useDefaultOrg
|
||||
}: TCompleteAccountSignupDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
@@ -293,15 +294,24 @@ export const authSignupServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!organizationId) {
|
||||
const newOrganization = await orgService.createOrganization({
|
||||
userId: user.id,
|
||||
userEmail: user.email ?? user.username,
|
||||
orgName: organizationName
|
||||
});
|
||||
let orgId = "";
|
||||
if (useDefaultOrg && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||
if (!defaultOrg) throw new BadRequestError({ message: "Failed to find default organization" });
|
||||
orgId = defaultOrg.id;
|
||||
} else {
|
||||
if (!organizationName) throw new BadRequestError({ message: "Organization name is required" });
|
||||
const newOrganization = await orgService.createOrganization({
|
||||
userId: user.id,
|
||||
userEmail: user.email ?? user.username,
|
||||
orgName: organizationName
|
||||
});
|
||||
|
||||
if (!newOrganization) throw new Error("Failed to create organization");
|
||||
if (!newOrganization) throw new Error("Failed to create organization");
|
||||
orgId = newOrganization.id;
|
||||
}
|
||||
|
||||
organizationId = newOrganization.id;
|
||||
organizationId = orgId;
|
||||
}
|
||||
|
||||
const updatedMembersips = await orgDAL.updateMembership(
|
||||
|
||||
@@ -12,12 +12,13 @@ export type TCompleteAccountSignupDTO = {
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
organizationName: string;
|
||||
organizationName?: string;
|
||||
providerAuthToken?: string | null;
|
||||
attributionSource?: string | undefined;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
authorization: string;
|
||||
useDefaultOrg?: boolean;
|
||||
};
|
||||
|
||||
export type TCompleteAccountInviteDTO = {
|
||||
|
||||
@@ -0,0 +1,706 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import axios from "axios";
|
||||
import { TeamsActivityHandler, TurnContext } from "botbuilder";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||
|
||||
const ConsentError = "AADSTS65001";
|
||||
|
||||
export const verifyTenantFromCode = async (
|
||||
tenantId: string,
|
||||
code: string,
|
||||
redirectUri: string,
|
||||
clientId: string,
|
||||
clientSecret: string
|
||||
) => {
|
||||
const getAccessToken = async (params: URLSearchParams) => {
|
||||
const response = await axios
|
||||
.post<{ access_token: string }>(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, params, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (axios.isAxiosError(err)) {
|
||||
if ((err.response?.data as { error_description?: string })?.error_description?.includes(ConsentError)) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to verify tenant, please ensure that you have granted admin consent."
|
||||
});
|
||||
}
|
||||
logger.error(err.response?.data, "Error fetching Microsoft Teams access token");
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
return response.data.access_token;
|
||||
};
|
||||
|
||||
// Azure App-based auth
|
||||
const applicationAccessToken = await getAccessToken(
|
||||
new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: "client_credentials"
|
||||
})
|
||||
);
|
||||
|
||||
// User-based auth
|
||||
const authorizationAccessToken = await getAccessToken(
|
||||
new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: "authorization_code",
|
||||
code
|
||||
})
|
||||
);
|
||||
|
||||
// Verify application token
|
||||
const { tid: tenantIdFromApplicationAccessToken } = jwt.decode(applicationAccessToken) as { tid: string };
|
||||
|
||||
if (tenantIdFromApplicationAccessToken !== tenantId) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid application token tenant ID. Expected ${tenantId}, got ${tenantIdFromApplicationAccessToken}`
|
||||
});
|
||||
}
|
||||
|
||||
// Verify user authorization token
|
||||
const { tid: tenantIdFromAuthorizationAccessToken } = jwt.decode(authorizationAccessToken) as { tid: string };
|
||||
|
||||
if (tenantIdFromAuthorizationAccessToken !== tenantId) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid authorization token tenant ID. Expected ${tenantId}, got ${tenantIdFromAuthorizationAccessToken}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getMicrosoftTeamsAccessToken = async (
|
||||
{
|
||||
orgId,
|
||||
microsoftTeamsIntegrationId,
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
getBotFrameworkToken
|
||||
}: {
|
||||
microsoftTeamsIntegrationId: string;
|
||||
orgId: string;
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||
getBotFrameworkToken?: boolean;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const details = getBotFrameworkToken
|
||||
? {
|
||||
uri: "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
|
||||
scope: "https://api.botframework.com/.default"
|
||||
}
|
||||
: {
|
||||
uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||
scope: "https://graph.microsoft.com/.default"
|
||||
};
|
||||
|
||||
const integration = await microsoftTeamsIntegrationDAL.findOne(
|
||||
{
|
||||
id: microsoftTeamsIntegrationId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
throw new BadRequestError({ message: "Microsoft Teams integration not found" });
|
||||
}
|
||||
|
||||
if (getBotFrameworkToken) {
|
||||
// If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one.
|
||||
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||
|
||||
if (
|
||||
integration.encryptedBotAccessToken &&
|
||||
integration.botAccessTokenExpiresAt &&
|
||||
integration.botAccessTokenExpiresAt > currentTime
|
||||
) {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const botAccessToken = decryptor({
|
||||
cipherTextBlob: integration.encryptedBotAccessToken
|
||||
});
|
||||
|
||||
return botAccessToken.toString();
|
||||
}
|
||||
} else {
|
||||
// If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one.
|
||||
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||
|
||||
if (
|
||||
integration.encryptedAccessToken &&
|
||||
integration.accessTokenExpiresAt &&
|
||||
integration.accessTokenExpiresAt > currentTime
|
||||
) {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const accessToken = decryptor({
|
||||
cipherTextBlob: integration.encryptedAccessToken
|
||||
});
|
||||
|
||||
return accessToken.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const tokenResponse = await axios.post<{ access_token: string; expires_in: number }>(
|
||||
details.uri,
|
||||
new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: details.scope,
|
||||
grant_type: "client_credentials"
|
||||
})
|
||||
);
|
||||
|
||||
if (getBotFrameworkToken) {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = encryptor({
|
||||
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||
});
|
||||
|
||||
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||
|
||||
await microsoftTeamsIntegrationDAL.update(
|
||||
{
|
||||
id: microsoftTeamsIntegrationId
|
||||
},
|
||||
{
|
||||
botAccessTokenExpiresAt: expiresAt,
|
||||
encryptedBotAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
} else {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedAccessToken } = encryptor({
|
||||
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||
});
|
||||
|
||||
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||
|
||||
await microsoftTeamsIntegrationDAL.update(
|
||||
{
|
||||
id: microsoftTeamsIntegrationId
|
||||
},
|
||||
{
|
||||
accessTokenExpiresAt: expiresAt,
|
||||
encryptedAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return tokenResponse.data.access_token;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(
|
||||
error.response?.data,
|
||||
`getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token [status-code=${error.response?.status}]`
|
||||
);
|
||||
} else {
|
||||
logger.error(error, "getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const isBotInstalledInTenant = async (
|
||||
{
|
||||
tenantId,
|
||||
botAppId,
|
||||
botAppPassword,
|
||||
botId,
|
||||
orgId,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
microsoftTeamsIntegrationId
|
||||
}: {
|
||||
tenantId: string;
|
||||
botAppId: string;
|
||||
botAppPassword: string;
|
||||
botId: string;
|
||||
orgId: string;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||
microsoftTeamsIntegrationId: string;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const botAccessToken = await getMicrosoftTeamsAccessToken(
|
||||
{
|
||||
tenantId,
|
||||
clientId: botAppId.toString(),
|
||||
clientSecret: botAppPassword.toString(),
|
||||
getBotFrameworkToken: true,
|
||||
orgId,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
microsoftTeamsIntegrationId
|
||||
},
|
||||
tx
|
||||
).catch(() => null);
|
||||
|
||||
const accessToken = await getMicrosoftTeamsAccessToken(
|
||||
{
|
||||
orgId,
|
||||
tenantId,
|
||||
clientId: botAppId.toString(),
|
||||
clientSecret: botAppPassword.toString(),
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
microsoftTeamsIntegrationId
|
||||
},
|
||||
tx
|
||||
).catch(() => null);
|
||||
|
||||
if (!botAccessToken || !accessToken) {
|
||||
return {
|
||||
accessToken: null,
|
||||
botAccessToken: null,
|
||||
installed: false,
|
||||
internalId: null
|
||||
} as const;
|
||||
}
|
||||
|
||||
const appsResponse = await axios
|
||||
.get<{ value: { id: string; displayName: string; distributionMethod: string; externalId: string }[] }>(
|
||||
"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error(error, "Error fetching installed apps");
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!appsResponse) {
|
||||
return {
|
||||
installed: false,
|
||||
internalId: null,
|
||||
accessToken,
|
||||
botAccessToken
|
||||
} as const;
|
||||
}
|
||||
|
||||
const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId);
|
||||
|
||||
if (!botInstalledInTenant) {
|
||||
return {
|
||||
installed: false,
|
||||
internalId: null,
|
||||
accessToken,
|
||||
botAccessToken
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
installed: true,
|
||||
internalId: botInstalledInTenant.id,
|
||||
accessToken,
|
||||
botAccessToken
|
||||
} as const;
|
||||
} catch (error) {
|
||||
logger.error(error, "Error fetching installed apps");
|
||||
return {
|
||||
installed: false,
|
||||
internalId: null,
|
||||
accessToken: null,
|
||||
botAccessToken: null
|
||||
} as const;
|
||||
}
|
||||
};
|
||||
|
||||
export const buildTeamsPayload = (notification: TNotification) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
switch (notification.type) {
|
||||
case TriggerFeature.SECRET_APPROVAL: {
|
||||
const { payload } = notification;
|
||||
|
||||
const adaptiveCard = {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.5",
|
||||
body: [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "Secret approval request",
|
||||
weight: "Bolder",
|
||||
size: "Large"
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `A secret approval request has been opened by ${payload.userEmail}.`,
|
||||
wrap: true
|
||||
},
|
||||
{
|
||||
type: "FactSet",
|
||||
facts: [
|
||||
{
|
||||
title: "Environment",
|
||||
value: payload.environment
|
||||
},
|
||||
{
|
||||
title: "Secret path",
|
||||
value: payload.secretPath || "/"
|
||||
},
|
||||
{
|
||||
title: `Secret Key${payload.secretKeys.length > 1 ? "s" : ""}`,
|
||||
value: payload.secretKeys.join(", ")
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "View request in Infisical",
|
||||
url: `${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${payload.requestId}`
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
adaptiveCard
|
||||
};
|
||||
}
|
||||
|
||||
case TriggerFeature.ACCESS_REQUEST: {
|
||||
const { payload } = notification;
|
||||
|
||||
const adaptiveCard = {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.5",
|
||||
body: [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: "New access approval request pending for review",
|
||||
weight: "Bolder",
|
||||
size: "Large"
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access to path '${payload.secretPath}' in the ${payload.environment} environment of ${
|
||||
payload.projectName
|
||||
} project.`,
|
||||
wrap: true
|
||||
},
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: `The following permissions are requested: ${payload.permissions.join(", ")}`,
|
||||
wrap: true
|
||||
},
|
||||
payload.note
|
||||
? {
|
||||
type: "TextBlock",
|
||||
text: `**User Note**: ${payload.note}`,
|
||||
wrap: true
|
||||
}
|
||||
: null
|
||||
].filter(Boolean),
|
||||
actions: [
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: "View request in Infisical",
|
||||
url: payload.approvalUrl
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
adaptiveCard
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new BadRequestError({
|
||||
message: "Teams notification type not supported."
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class TeamsBot extends TeamsActivityHandler {
|
||||
private botAppId: string;
|
||||
|
||||
private botAppPassword: string;
|
||||
|
||||
private workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||
|
||||
private microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||
|
||||
constructor({
|
||||
botAppId,
|
||||
botAppPassword,
|
||||
workflowIntegrationDAL,
|
||||
microsoftTeamsIntegrationDAL
|
||||
}: {
|
||||
botAppId: string;
|
||||
botAppPassword: string;
|
||||
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.botAppId = botAppId;
|
||||
this.botAppPassword = botAppPassword;
|
||||
this.workflowIntegrationDAL = workflowIntegrationDAL;
|
||||
this.microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDAL;
|
||||
|
||||
// We know when a bot is added, but we can't know when it's fully removed from the tenant.
|
||||
this.onTeamsMembersAddedEvent(async (membersAdded, _, context) => {
|
||||
const botWasAdded = membersAdded.some((member) => member.id === context.activity.recipient.id);
|
||||
|
||||
if (botWasAdded && context.activity.conversation.tenantId) {
|
||||
const microsoftTeamIntegration = await this.microsoftTeamsIntegrationDAL
|
||||
.findOne({
|
||||
tenantId: context.activity.conversation.tenantId
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (microsoftTeamIntegration) {
|
||||
await this.workflowIntegrationDAL
|
||||
.update(
|
||||
{
|
||||
id: microsoftTeamIntegration.id,
|
||||
status: WorkflowIntegrationStatus.PENDING
|
||||
},
|
||||
{
|
||||
status: WorkflowIntegrationStatus.INSTALLED
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error(error, "Microsoft Teams Workflow Integration: Failed to update workflow integration");
|
||||
});
|
||||
}
|
||||
|
||||
// This is required in order for the bot to send proactive messages, which is required for the bot to pass the bot release validation step.
|
||||
await context.sendActivity(
|
||||
"👋 Thanks for installing the Infisical app! You can now use the bot to send notifications to your selected teams."
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(context: TurnContext) {
|
||||
logger.info(context, "Processing Microsoft Teams context");
|
||||
await super.run(context);
|
||||
}
|
||||
|
||||
async sendMessageToChannel(
|
||||
botAccessToken: string,
|
||||
tenantId: string,
|
||||
channelId: string,
|
||||
teamId: string,
|
||||
notification: TNotification
|
||||
) {
|
||||
try {
|
||||
const { adaptiveCard } = buildTeamsPayload(notification);
|
||||
|
||||
const adaptiveCardActivity = {
|
||||
type: "message",
|
||||
attachments: [
|
||||
{
|
||||
contentType: "application/vnd.microsoft.card.adaptive",
|
||||
content: adaptiveCard
|
||||
}
|
||||
],
|
||||
conversation: {
|
||||
id: channelId,
|
||||
isGroup: true
|
||||
},
|
||||
channelData: {
|
||||
channel: {
|
||||
id: channelId
|
||||
},
|
||||
team: {
|
||||
id: teamId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await axios.post(
|
||||
`https://smba.trafficmanager.net/amer/v3/conversations/${channelId}/activities`,
|
||||
adaptiveCardActivity,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${botAccessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(
|
||||
error.response?.data,
|
||||
`sendMessageToChannel: Axios Error, Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]`
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
error,
|
||||
`sendMessageToChannel: Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTeamsAndChannels(accessToken: string, internalAppId: string) {
|
||||
try {
|
||||
let teamsNextLink: string = "https://graph.microsoft.com/v1.0/teams";
|
||||
|
||||
let allTeams: { displayName: string; id: string }[] = [];
|
||||
while (teamsNextLink?.length) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await axios.get<{
|
||||
value: { displayName: string; id: string }[];
|
||||
"@odata.nextLink"?: string;
|
||||
}>(teamsNextLink, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
allTeams = allTeams.concat(response.data.value);
|
||||
teamsNextLink = response.data["@odata.nextLink"] || "";
|
||||
} catch (error) {
|
||||
logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch teams");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for await (const team of allTeams) {
|
||||
try {
|
||||
// Get installed apps for this team
|
||||
const installedAppsResponse = await axios.get<{ value: { teamsAppDefinition: { teamsAppId: string } }[] }>(
|
||||
`https://graph.microsoft.com/v1.0/teams/${team.id}/installedApps?$expand=teamsAppDefinition`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!installedAppsResponse.data.value.some((app) => app.teamsAppDefinition.teamsAppId === internalAppId)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue; // skip this team if we can't determine if the bot is installed
|
||||
}
|
||||
|
||||
let allChannels: { displayName: string; id: string }[] = [];
|
||||
|
||||
let channelNextLink: string = `https://graph.microsoft.com/v1.0/teams/${team.id}/channels`;
|
||||
|
||||
while (channelNextLink?.length) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await axios
|
||||
.get<{
|
||||
value: { displayName: string; id: string }[];
|
||||
"@odata.nextLink"?: string;
|
||||
}>(channelNextLink, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(
|
||||
error.response?.data,
|
||||
"getTeamsAndChannels: Axios error, Microsoft Teams Workflow Integration: Failed to fetch channels"
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
error,
|
||||
"getTeamsAndChannels: Microsoft Teams Workflow Integration: Failed to fetch channels"
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
allChannels = allChannels.concat(resp.data.value);
|
||||
channelNextLink = resp.data["@odata.nextLink"] || "";
|
||||
}
|
||||
|
||||
const channels = allChannels.map((channel) => ({
|
||||
channelName: channel.displayName,
|
||||
channelId: channel.id
|
||||
}));
|
||||
|
||||
result.push({
|
||||
teamId: team.id,
|
||||
teamName: team.displayName,
|
||||
channels
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(error, "Microsoft Teams Workflow Integration: Error fetching teams and channels");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const validateMicrosoftTeamsChannelsSchema = z
|
||||
.object({
|
||||
teamId: z.string(),
|
||||
channelIds: z.array(z.string()).min(1)
|
||||
})
|
||||
.optional()
|
||||
.refine((data) => data === undefined || data?.channelIds.length <= 20, {
|
||||
message: "You can only select up to 20 Microsoft Teams channels"
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TMicrosoftTeamsIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TMicrosoftTeamsIntegrationDALFactory = ReturnType<typeof microsoftTeamsIntegrationDALFactory>;
|
||||
|
||||
export const microsoftTeamsIntegrationDALFactory = (db: TDbClient) => {
|
||||
const microsoftTeamsIntegrationOrm = ormify(db, TableName.MicrosoftTeamsIntegrations);
|
||||
|
||||
const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(`${TableName.WorkflowIntegrations}.id`, id)
|
||||
.first();
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
const findWithWorkflowIntegrationDetails = async (
|
||||
filter: Partial<TMicrosoftTeamsIntegrations> & Partial<TWorkflowIntegrations>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(filter);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...microsoftTeamsIntegrationOrm,
|
||||
findByIdWithWorkflowIntegrationDetails,
|
||||
findWithWorkflowIntegrationDetails
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,710 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import {
|
||||
CloudAdapter,
|
||||
ConfigurationBotFrameworkAuthentication,
|
||||
ConfigurationServiceClientCredentialFactory,
|
||||
Request,
|
||||
Response
|
||||
} from "botbuilder";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal";
|
||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||
import {
|
||||
getMicrosoftTeamsAccessToken,
|
||||
isBotInstalledInTenant,
|
||||
TeamsBot,
|
||||
verifyTenantFromCode
|
||||
} from "./microsoft-teams-fns";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||
import {
|
||||
TCheckInstallationStatusDTO,
|
||||
TCreateMicrosoftTeamsIntegrationDTO,
|
||||
TDeleteMicrosoftTeamsIntegrationDTO,
|
||||
TGetClientIdDTO,
|
||||
TGetMicrosoftTeamsIntegrationByIdDTO,
|
||||
TGetMicrosoftTeamsIntegrationByOrgDTO,
|
||||
TGetTeamsDTO,
|
||||
TSendNotificationDTO,
|
||||
TUpdateMicrosoftTeamsIntegrationDTO
|
||||
} from "./microsoft-teams-types";
|
||||
|
||||
function requestBodyToRecord(body: unknown): Record<string, unknown> {
|
||||
// if body is null or undefined, return an empty object
|
||||
if (body === null || body === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// if body is not an object or is an array, return an empty object
|
||||
if (typeof body !== "object" || Array.isArray(body)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// at this point, we know body is an object, so safe to cast
|
||||
return body as Record<string, unknown>;
|
||||
}
|
||||
|
||||
type TMicrosoftTeamsServiceFactoryDep = {
|
||||
microsoftTeamsIntegrationDAL: Pick<
|
||||
TMicrosoftTeamsIntegrationDALFactory,
|
||||
| "deleteById"
|
||||
| "updateById"
|
||||
| "create"
|
||||
| "findOne"
|
||||
| "findById"
|
||||
| "findByIdWithWorkflowIntegrationDetails"
|
||||
| "findWithWorkflowIntegrationDetails"
|
||||
| "update"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
||||
workflowIntegrationDAL: Pick<
|
||||
TWorkflowIntegrationDALFactory,
|
||||
"transaction" | "create" | "updateById" | "deleteById" | "update" | "findOne"
|
||||
>;
|
||||
serverCfgDAL: Pick<TSuperAdminDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TMicrosoftTeamsServiceFactory = ReturnType<typeof microsoftTeamsServiceFactory>;
|
||||
|
||||
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
export const microsoftTeamsServiceFactory = ({
|
||||
permissionService,
|
||||
serverCfgDAL,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
workflowIntegrationDAL
|
||||
}: TMicrosoftTeamsServiceFactoryDep) => {
|
||||
let teamsBot: TeamsBot | null = null;
|
||||
let adapter: CloudAdapter | null = null;
|
||||
|
||||
const initializeTeamsBot = async ({ botAppId, botAppPassword }: { botAppId: string; botAppPassword: string }) => {
|
||||
logger.info("Initializing Microsoft Teams bot");
|
||||
teamsBot = new TeamsBot({
|
||||
botAppId,
|
||||
botAppPassword,
|
||||
workflowIntegrationDAL,
|
||||
microsoftTeamsIntegrationDAL
|
||||
});
|
||||
|
||||
adapter = new CloudAdapter(
|
||||
new ConfigurationBotFrameworkAuthentication(
|
||||
{},
|
||||
new ConfigurationServiceClientCredentialFactory({
|
||||
MicrosoftAppId: botAppId,
|
||||
MicrosoftAppPassword: botAppPassword,
|
||||
MicrosoftAppType: "MultiTenant"
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (
|
||||
serverCfg?.encryptedMicrosoftTeamsAppId &&
|
||||
serverCfg?.encryptedMicrosoftTeamsClientSecret &&
|
||||
serverCfg?.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
|
||||
await initializeTeamsBot({
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString()
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err, "Error initializing Microsoft Teams bot on startup");
|
||||
}
|
||||
};
|
||||
|
||||
const checkInstallationStatus = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workflowIntegrationId
|
||||
}: TCheckInstallationStatusDTO) => {
|
||||
const microsoftTeamsIntegration =
|
||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||
});
|
||||
}
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||
|
||||
const teamsBotInfo = await isBotInstalledInTenant({
|
||||
tenantId: microsoftTeamsIntegration.tenantId,
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString(),
|
||||
botId: decryptedBotId.toString(),
|
||||
orgId: microsoftTeamsIntegration.orgId,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||
});
|
||||
|
||||
if (!teamsBotInfo.installed) {
|
||||
if (microsoftTeamsIntegration.status === WorkflowIntegrationStatus.INSTALLED) {
|
||||
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||
status: WorkflowIntegrationStatus.PENDING
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||
});
|
||||
}
|
||||
|
||||
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
|
||||
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||
status: WorkflowIntegrationStatus.INSTALLED
|
||||
});
|
||||
}
|
||||
|
||||
return microsoftTeamsIntegration;
|
||||
};
|
||||
|
||||
const completeMicrosoftTeamsIntegration = async ({
|
||||
code,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
tenantId,
|
||||
slug,
|
||||
description,
|
||||
redirectUri
|
||||
}: TCreateMicrosoftTeamsIntegrationDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
const { encryptedMicrosoftTeamsAppId, encryptedMicrosoftTeamsClientSecret, encryptedMicrosoftTeamsBotId } =
|
||||
serverCfg;
|
||||
|
||||
if (!encryptedMicrosoftTeamsAppId || !encryptedMicrosoftTeamsClientSecret || !encryptedMicrosoftTeamsBotId) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||
});
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId);
|
||||
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
||||
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
||||
|
||||
await verifyTenantFromCode(tenantId, code, redirectUri, botAppId.toString(), botAppPassword.toString());
|
||||
|
||||
await workflowIntegrationDAL.transaction(async (tx) => {
|
||||
const workflowIntegration = await workflowIntegrationDAL.create(
|
||||
{
|
||||
description,
|
||||
orgId: actorOrgId,
|
||||
slug,
|
||||
integration: WorkflowIntegration.MICROSOFT_TEAMS,
|
||||
status: WorkflowIntegrationStatus.PENDING
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL
|
||||
.create(
|
||||
{
|
||||
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
|
||||
id: workflowIntegration.id,
|
||||
tenantId
|
||||
},
|
||||
tx
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err instanceof DatabaseError) {
|
||||
if ((err.error as Error)?.stack?.includes("duplicate key value violates unique constraint"))
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams integration with the same Tenant ID already exists."
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
const teamsBotInfo = await isBotInstalledInTenant(
|
||||
{
|
||||
tenantId: microsoftTeamsIntegration.tenantId,
|
||||
botAppId: botAppId.toString(),
|
||||
botAppPassword: botAppPassword.toString(),
|
||||
botId: botId.toString(),
|
||||
orgId: workflowIntegration.orgId,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (teamsBotInfo.installed) {
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId: workflowIntegration.orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
const { cipherTextBlob: encryptedAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(teamsBotInfo.accessToken, "utf8")
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(teamsBotInfo.botAccessToken, "utf8")
|
||||
});
|
||||
await microsoftTeamsIntegrationDAL.updateById(
|
||||
microsoftTeamsIntegration.id,
|
||||
{
|
||||
internalTeamsAppId: teamsBotInfo.internalId,
|
||||
encryptedAccessToken,
|
||||
encryptedBotAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await workflowIntegrationDAL.updateById(
|
||||
workflowIntegration.id,
|
||||
{
|
||||
status: WorkflowIntegrationStatus.INSTALLED
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
if (!serverCfg.encryptedMicrosoftTeamsAppId) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID is not set"
|
||||
});
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const clientId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
|
||||
return clientId.toString();
|
||||
};
|
||||
const getMicrosoftTeamsIntegrationsByOrg = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TGetMicrosoftTeamsIntegrationByOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const microsoftTeamsIntegrations = await microsoftTeamsIntegrationDAL.findWithWorkflowIntegrationDetails({
|
||||
orgId: actorOrgId,
|
||||
status: WorkflowIntegrationStatus.INSTALLED
|
||||
});
|
||||
|
||||
return microsoftTeamsIntegrations.map((integration) => ({
|
||||
...integration,
|
||||
status: integration.status as WorkflowIntegrationStatus,
|
||||
tenantId: integration.tenantId
|
||||
}));
|
||||
};
|
||||
|
||||
const getMicrosoftTeamsIntegrationById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TGetMicrosoftTeamsIntegrationByIdDTO) => {
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Microsoft Teams integration not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
return {
|
||||
...microsoftTeamsIntegration,
|
||||
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||
};
|
||||
};
|
||||
|
||||
const updateMicrosoftTeamsIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id,
|
||||
slug,
|
||||
description
|
||||
}: TUpdateMicrosoftTeamsIntegrationDTO) => {
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${id} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const updatedIntegration = await workflowIntegrationDAL.transaction(async (tx) => {
|
||||
await workflowIntegrationDAL.updateById(
|
||||
microsoftTeamsIntegration.id,
|
||||
{
|
||||
slug,
|
||||
description
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const integration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
|
||||
microsoftTeamsIntegration.id,
|
||||
tx
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${microsoftTeamsIntegration.id} not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...integration,
|
||||
status: integration.status as WorkflowIntegrationStatus
|
||||
};
|
||||
});
|
||||
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
const deleteMicrosoftTeamsIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TDeleteMicrosoftTeamsIntegrationDTO) => {
|
||||
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${id} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
await workflowIntegrationDAL.deleteById(id);
|
||||
|
||||
return {
|
||||
...microsoftTeamsIntegration,
|
||||
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||
};
|
||||
};
|
||||
|
||||
const getTeams = async ({ actorId, actor, actorOrgId, actorAuthMethod, workflowIntegrationId }: TGetTeamsDTO) => {
|
||||
const microsoftTeamsIntegration =
|
||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
microsoftTeamsIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
if (!teamsBot || !adapter) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to get teams and channels because the Microsoft Teams bot is uninitialized"
|
||||
});
|
||||
}
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||
});
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||
|
||||
const { installed, internalId, accessToken } = await isBotInstalledInTenant({
|
||||
tenantId: microsoftTeamsIntegration.tenantId,
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString(),
|
||||
botId: decryptedBotId.toString(),
|
||||
orgId: microsoftTeamsIntegration.orgId,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||
});
|
||||
|
||||
if (!installed) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||
});
|
||||
}
|
||||
|
||||
const teams = await teamsBot.getTeamsAndChannels(accessToken, internalId);
|
||||
|
||||
return {
|
||||
...microsoftTeamsIntegration,
|
||||
teams
|
||||
};
|
||||
};
|
||||
|
||||
const handleMessageEndpoint = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
if (!teamsBot || !adapter) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to handle message endpoint because the Microsoft Teams bot is uninitialized"
|
||||
});
|
||||
}
|
||||
|
||||
// We need to manually build a Response object because the BotFrameworkAdapter expects a Response object. We are using FastifyReply as the underlying socket.
|
||||
const response: Response = {
|
||||
socket: res.raw.socket,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
end(...args: any[]): unknown {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
res.raw.end(...args);
|
||||
return this;
|
||||
},
|
||||
|
||||
header(name: string, value: unknown): unknown {
|
||||
res.raw.setHeader(name, value as string);
|
||||
return this;
|
||||
},
|
||||
|
||||
send(...args: unknown[]): unknown {
|
||||
// For the first argument, which is typically the body
|
||||
if (args.length > 0) {
|
||||
const body = args[0];
|
||||
|
||||
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
||||
res.raw.write(body);
|
||||
} else if (body !== null && body !== undefined) {
|
||||
const json = JSON.stringify(body);
|
||||
if (!res.raw.headersSent && !res.raw.getHeader("content-type")) {
|
||||
res.raw.setHeader("content-type", "application/json");
|
||||
}
|
||||
res.raw.write(json);
|
||||
}
|
||||
}
|
||||
|
||||
const lastArg = args[args.length - 1];
|
||||
if (typeof lastArg === "function") {
|
||||
lastArg();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
status(code: number): unknown {
|
||||
res.raw.statusCode = code;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
const request: Request = {
|
||||
body: requestBodyToRecord(req.body),
|
||||
headers: req.headers,
|
||||
method: req.method
|
||||
};
|
||||
|
||||
await adapter.process(request, response, async (context) => {
|
||||
await teamsBot?.run(context);
|
||||
});
|
||||
};
|
||||
|
||||
const sendNotification = async ({
|
||||
tenantId,
|
||||
target,
|
||||
notification,
|
||||
orgId,
|
||||
microsoftTeamsIntegrationId
|
||||
}: TSendNotificationDTO) => {
|
||||
if (!teamsBot || !adapter) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to send notification because the Microsoft Teams bot is uninitialized"
|
||||
});
|
||||
}
|
||||
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||
});
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const botAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const botAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
|
||||
const botAccessToken = await getMicrosoftTeamsAccessToken({
|
||||
tenantId,
|
||||
clientId: botAppId.toString(),
|
||||
clientSecret: botAppPassword.toString(),
|
||||
getBotFrameworkToken: true,
|
||||
orgId,
|
||||
kmsService,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
microsoftTeamsIntegrationId
|
||||
});
|
||||
|
||||
for await (const channelId of target.channelIds) {
|
||||
await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, notification);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getMicrosoftTeamsIntegrationsByOrg,
|
||||
getMicrosoftTeamsIntegrationById,
|
||||
updateMicrosoftTeamsIntegration,
|
||||
deleteMicrosoftTeamsIntegration,
|
||||
completeMicrosoftTeamsIntegration,
|
||||
initializeTeamsBot,
|
||||
getTeams,
|
||||
handleMessageEndpoint,
|
||||
start,
|
||||
sendNotification,
|
||||
checkInstallationStatus,
|
||||
getClientId
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||
|
||||
export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetClientIdDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
redirectUri: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetMicrosoftTeamsIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateMicrosoftTeamsIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||
TOrgPermission,
|
||||
"orgId"
|
||||
>;
|
||||
|
||||
export type TGetTeamsDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
workflowIntegrationId: string;
|
||||
};
|
||||
|
||||
export type TDeleteMicrosoftTeamsIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TSendNotificationDTO = {
|
||||
tenantId: string;
|
||||
microsoftTeamsIntegrationId: string;
|
||||
orgId: string;
|
||||
target: {
|
||||
teamId: string;
|
||||
channelIds: string[];
|
||||
};
|
||||
notification: TNotification;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType<typeof projectMicrosoftTeamsConfigDALFactory>;
|
||||
|
||||
export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => {
|
||||
const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs);
|
||||
|
||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||
return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs)
|
||||
.join(
|
||||
TableName.MicrosoftTeamsIntegrations,
|
||||
`${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`,
|
||||
`${TableName.MicrosoftTeamsIntegrations}.id`
|
||||
)
|
||||
.where("projectId", "=", projectId)
|
||||
.select(
|
||||
selectAllTableCols(TableName.ProjectMicrosoftTeamsConfigs),
|
||||
selectAllTableCols(TableName.MicrosoftTeamsIntegrations)
|
||||
)
|
||||
.first();
|
||||
};
|
||||
|
||||
return { ...projectMicrosoftTeamsConfigOrm, getIntegrationDetailsByProject };
|
||||
};
|
||||
@@ -44,6 +44,9 @@ import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||
@@ -61,9 +64,11 @@ import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||
import { validateSlackChannelsField } from "../slack/slack-auth-validators";
|
||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
||||
import { TProjectQueueFactory } from "./project-queue";
|
||||
@@ -71,10 +76,11 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
||||
import {
|
||||
TCreateProjectDTO,
|
||||
TDeleteProjectDTO,
|
||||
TDeleteProjectWorkflowIntegration,
|
||||
TGetProjectDTO,
|
||||
TGetProjectKmsKey,
|
||||
TGetProjectSlackConfig,
|
||||
TGetProjectSshConfig,
|
||||
TGetProjectWorkflowIntegrationConfig,
|
||||
TListProjectAlertsDTO,
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
@@ -93,9 +99,9 @@ import {
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectKmsDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectSlackConfig,
|
||||
TUpdateProjectSshConfig,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
TUpdateProjectWorkflowIntegration,
|
||||
TUpgradeProjectDTO
|
||||
} from "./project-types";
|
||||
|
||||
@@ -124,8 +130,19 @@ type TProjectServiceFactoryDep = {
|
||||
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
||||
>;
|
||||
groupProjectDAL: Pick<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">;
|
||||
@@ -199,7 +216,9 @@ export const projectServiceFactory = ({
|
||||
kmsService,
|
||||
projectBotDAL,
|
||||
projectSlackConfigDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
slackIntegrationDAL,
|
||||
microsoftTeamsIntegrationDAL,
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService
|
||||
@@ -1480,13 +1499,14 @@ export const projectServiceFactory = ({
|
||||
return projectSshConfig;
|
||||
};
|
||||
|
||||
const getProjectSlackConfig = async ({
|
||||
const getProjectWorkflowIntegrationConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TGetProjectSlackConfig) => {
|
||||
projectId,
|
||||
integration
|
||||
}: TGetProjectWorkflowIntegrationConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
@@ -1505,23 +1525,60 @@ export const projectServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
|
||||
return projectSlackConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
if (integration === WorkflowIntegration.SLACK) {
|
||||
const config = await projectSlackConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
throw new NotFoundError({
|
||||
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
integration,
|
||||
integrationId: config.slackIntegrationId
|
||||
};
|
||||
}
|
||||
|
||||
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||
const config = await projectMicrosoftTeamsConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
throw new NotFoundError({
|
||||
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
integration,
|
||||
integrationId: config.microsoftTeamsIntegrationId
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Integration type '${integration as string}' not supported`
|
||||
});
|
||||
};
|
||||
|
||||
const updateProjectSlackConfig = async ({
|
||||
const updateProjectWorkflowIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
integration,
|
||||
integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
}: TUpdateProjectSlackConfig) => {
|
||||
}: TUpdateProjectWorkflowIntegration) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
@@ -1529,17 +1586,206 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Slack integration with ID '${slackIntegrationId}' not found`
|
||||
if (integration === WorkflowIntegration.SLACK) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
||||
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
||||
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Slack integration with ID '${integrationId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (slackIntegration.orgId !== actorOrgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
if (slackIntegration.orgId !== project.orgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
const updatedWorkflowIntegration = await projectSlackConfigDAL.transaction(async (tx) => {
|
||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (slackConfig) {
|
||||
return projectSlackConfigDAL.updateById(
|
||||
slackConfig.id,
|
||||
{
|
||||
slackIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
slackIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedWorkflowIntegration,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||
integrationId: slackIntegration.id,
|
||||
integration: WorkflowIntegration.SLACK
|
||||
} as const;
|
||||
}
|
||||
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
if (isAccessRequestNotificationEnabled && !accessRequestChannels) {
|
||||
throw new BadRequestError({
|
||||
message: "Access request channels are required when access request notifications are enabled"
|
||||
});
|
||||
}
|
||||
|
||||
if (isSecretRequestNotificationEnabled && !secretRequestChannels) {
|
||||
throw new BadRequestError({
|
||||
message: "Secret request channels are required when secret request notifications are enabled"
|
||||
});
|
||||
}
|
||||
|
||||
if (!secretRequestChannels && !accessRequestChannels) {
|
||||
throw new BadRequestError({
|
||||
message: "At least one of access request channels or secret request channels is required"
|
||||
});
|
||||
}
|
||||
|
||||
const microsoftTeamsIntegration =
|
||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||
|
||||
if (!microsoftTeamsIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: `Microsoft Teams integration with ID '${integrationId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
|
||||
throw new BadRequestError({
|
||||
message: "Microsoft Teams integration is not properly installed in your tenant."
|
||||
});
|
||||
}
|
||||
|
||||
if (microsoftTeamsIntegration.orgId !== actorOrgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected Microsoft Teams integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
if (microsoftTeamsIntegration.orgId !== project.orgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected Microsoft Teams integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
const sanitizedAccessRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
|
||||
const sanitizedSecretRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
|
||||
|
||||
const updatedWorkflowIntegration = await projectMicrosoftTeamsConfigDAL.transaction(async (tx) => {
|
||||
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (microsoftTeamsConfig) {
|
||||
return projectMicrosoftTeamsConfigDAL.updateById(
|
||||
microsoftTeamsConfig.id,
|
||||
{
|
||||
microsoftTeamsIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return projectMicrosoftTeamsConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
microsoftTeamsIntegrationId: integrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedWorkflowIntegration,
|
||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||
integrationId: microsoftTeamsIntegration.id,
|
||||
integration: WorkflowIntegration.MICROSOFT_TEAMS
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (slackIntegration.orgId !== actorOrgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
throw new BadRequestError({
|
||||
message: `Integration type '${integration as string}' not supported`
|
||||
});
|
||||
};
|
||||
|
||||
const deleteProjectWorkflowIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
integrationId,
|
||||
integration
|
||||
}: TDeleteProjectWorkflowIntegration) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `Project with ID '${projectId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1552,47 +1798,28 @@ export const projectServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||
|
||||
if (slackIntegration.orgId !== project.orgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
if (integration === WorkflowIntegration.SLACK) {
|
||||
const [deletedIntegration] = await projectSlackConfigDAL.delete({
|
||||
projectId,
|
||||
slackIntegrationId: integrationId
|
||||
});
|
||||
|
||||
return deletedIntegration;
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.transaction(async (tx) => {
|
||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||
const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({
|
||||
projectId,
|
||||
microsoftTeamsIntegrationId: integrationId
|
||||
});
|
||||
|
||||
if (slackConfig) {
|
||||
return projectSlackConfigDAL.updateById(
|
||||
slackConfig.id,
|
||||
{
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return deletedIntegration;
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
throw new BadRequestError({
|
||||
message: `Integration with ID '${integrationId}' not found`
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1702,10 +1929,11 @@ export const projectServiceFactory = ({
|
||||
getProjectKmsBackup,
|
||||
loadProjectKmsBackup,
|
||||
getProjectKmsKeys,
|
||||
getProjectWorkflowIntegrationConfig,
|
||||
updateProjectWorkflowIntegration,
|
||||
deleteProjectWorkflowIntegration,
|
||||
getProjectSshConfig,
|
||||
updateProjectSshConfig,
|
||||
getProjectSlackConfig,
|
||||
updateProjectSlackConfig,
|
||||
requestProjectAccess,
|
||||
searchProjects
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const HC_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Hashicorp Vault",
|
||||
destination: SecretSync.HCVault,
|
||||
connection: AppConnection.HCVault,
|
||||
canImportSecrets: true
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { getHCVaultAccessToken, getHCVaultInstanceUrl } from "@app/services/app-connection/hc-vault";
|
||||
import {
|
||||
THCVaultListVariables,
|
||||
THCVaultListVariablesResponse,
|
||||
THCVaultSyncWithCredentials,
|
||||
TPostHCVaultVariable
|
||||
} from "@app/services/secret-sync/hc-vault/hc-vault-sync-types";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => {
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
try {
|
||||
const { data } = await request.get<THCVaultListVariablesResponse>(
|
||||
`${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`,
|
||||
{
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
...(namespace ? { "X-Vault-Namespace": namespace } : {})
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data.data.data;
|
||||
} catch (error: unknown) {
|
||||
// Returning an empty set when a path isn't found allows that path to be created by a later POST request
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Hashicorp Vault updates all variables in one batch. This is to respect their versioning
|
||||
const updateHCVaultVariables = async ({
|
||||
path,
|
||||
instanceUrl,
|
||||
namespace,
|
||||
accessToken,
|
||||
mount,
|
||||
data
|
||||
}: TPostHCVaultVariable) => {
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
return request.post(
|
||||
`${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`,
|
||||
{
|
||||
data
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
...(namespace ? { "X-Vault-Namespace": namespace } : {}),
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const HCVaultSyncFns = {
|
||||
syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { mount, path },
|
||||
syncOptions: { disableSecretDeletion }
|
||||
} = secretSync;
|
||||
|
||||
const { namespace } = connection.credentials;
|
||||
const accessToken = await getHCVaultAccessToken(connection);
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
|
||||
const variables = await listHCVaultVariables({
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
namespace,
|
||||
mount,
|
||||
path
|
||||
});
|
||||
let tainted = false;
|
||||
|
||||
for (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
if (value !== variables[key]) {
|
||||
variables[key] = value;
|
||||
tainted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
for await (const [key] of Object.entries(variables)) {
|
||||
if (!(key in secretMap)) {
|
||||
delete variables[key];
|
||||
tainted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only update variables if there was a change detected
|
||||
if (!tainted) return;
|
||||
|
||||
try {
|
||||
await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables });
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { mount, path }
|
||||
} = secretSync;
|
||||
|
||||
const { namespace } = connection.credentials;
|
||||
const accessToken = await getHCVaultAccessToken(connection);
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
|
||||
const variables = await listHCVaultVariables({ instanceUrl, namespace, accessToken, mount, path });
|
||||
|
||||
for await (const [key] of Object.entries(variables)) {
|
||||
if (key in secretMap) {
|
||||
delete variables[key];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables });
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error
|
||||
});
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: THCVaultSyncWithCredentials) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { mount, path }
|
||||
} = secretSync;
|
||||
|
||||
const { namespace } = connection.credentials;
|
||||
const accessToken = await getHCVaultAccessToken(connection);
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
|
||||
const variables = await listHCVaultVariables({
|
||||
instanceUrl,
|
||||
namespace,
|
||||
accessToken,
|
||||
mount,
|
||||
path
|
||||
});
|
||||
|
||||
return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }]));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const HCVaultSyncDestinationConfigSchema = z.object({
|
||||
mount: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Secrets Engine Mount required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.mount),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Path required")
|
||||
.transform((val) => val.replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes
|
||||
.refine((val) => new RE2("^([a-zA-Z0-9._-]+/)*[a-zA-Z0-9._-]+$").test(val), {
|
||||
message:
|
||||
"Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments."
|
||||
})
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.path)
|
||||
});
|
||||
|
||||
const HCVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const HCVaultSyncSchema = BaseSecretSyncSchema(SecretSync.HCVault, HCVaultSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.HCVault),
|
||||
destinationConfig: HCVaultSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateHCVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.HCVault,
|
||||
HCVaultSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: HCVaultSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateHCVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.HCVault,
|
||||
HCVaultSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: HCVaultSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const HCVaultSyncListItemSchema = z.object({
|
||||
name: z.literal("Hashicorp Vault"),
|
||||
connection: z.literal(AppConnection.HCVault),
|
||||
destination: z.literal(SecretSync.HCVault),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { THCVaultConnection } from "@app/services/app-connection/hc-vault";
|
||||
|
||||
import { CreateHCVaultSyncSchema, HCVaultSyncListItemSchema, HCVaultSyncSchema } from "./hc-vault-sync-schemas";
|
||||
|
||||
export type THCVaultSync = z.infer<typeof HCVaultSyncSchema>;
|
||||
|
||||
export type THCVaultSyncInput = z.infer<typeof CreateHCVaultSyncSchema>;
|
||||
|
||||
export type THCVaultSyncListItem = z.infer<typeof HCVaultSyncListItemSchema>;
|
||||
|
||||
export type THCVaultSyncWithCredentials = THCVaultSync & {
|
||||
connection: THCVaultConnection;
|
||||
};
|
||||
|
||||
export type THCVaultListVariablesResponse = {
|
||||
data: {
|
||||
data: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type THCVaultListVariables = {
|
||||
accessToken: string;
|
||||
instanceUrl: string;
|
||||
namespace?: string;
|
||||
mount: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type TPostHCVaultVariable = THCVaultListVariables & {
|
||||
data: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TDeleteHCVaultVariable = THCVaultListVariables;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./hc-vault-sync-constants";
|
||||
export * from "./hc-vault-sync-fns";
|
||||
export * from "./hc-vault-sync-schemas";
|
||||
export * from "./hc-vault-sync-types";
|
||||
@@ -11,6 +11,7 @@ export enum SecretSync {
|
||||
Camunda = "camunda",
|
||||
Vercel = "vercel",
|
||||
Windmill = "windmill",
|
||||
HCVault = "hashicorp-vault",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az
|
||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
|
||||
@@ -45,6 +46,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
|
||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
|
||||
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
|
||||
[SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION,
|
||||
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
@@ -142,6 +144,8 @@ export const SecretSyncFns = {
|
||||
return VercelSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.Windmill:
|
||||
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.HCVault:
|
||||
return HCVaultSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.TeamCity:
|
||||
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
@@ -203,6 +207,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Windmill:
|
||||
secretMap = await WindmillSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.HCVault:
|
||||
secretMap = await HCVaultSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.TeamCity:
|
||||
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
@@ -259,6 +266,8 @@ export const SecretSyncFns = {
|
||||
return VercelSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.Windmill:
|
||||
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.HCVault:
|
||||
return HCVaultSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.TeamCity:
|
||||
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
|
||||
@@ -14,6 +14,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Camunda]: "Camunda",
|
||||
[SecretSync.Vercel]: "Vercel",
|
||||
[SecretSync.Windmill]: "Windmill",
|
||||
[SecretSync.HCVault]: "Hashicorp Vault",
|
||||
[SecretSync.TeamCity]: "TeamCity"
|
||||
};
|
||||
|
||||
@@ -30,5 +31,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Camunda]: AppConnection.Camunda,
|
||||
[SecretSync.Vercel]: AppConnection.Vercel,
|
||||
[SecretSync.Windmill]: AppConnection.Windmill,
|
||||
[SecretSync.HCVault]: AppConnection.HCVault,
|
||||
[SecretSync.TeamCity]: AppConnection.TeamCity
|
||||
};
|
||||
|
||||
@@ -55,6 +55,12 @@ import {
|
||||
TAzureKeyVaultSyncWithCredentials
|
||||
} from "./azure-key-vault";
|
||||
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||
import {
|
||||
THCVaultSync,
|
||||
THCVaultSyncInput,
|
||||
THCVaultSyncListItem,
|
||||
THCVaultSyncWithCredentials
|
||||
} from "./hc-vault/hc-vault-sync-types";
|
||||
import {
|
||||
THumanitecSync,
|
||||
THumanitecSyncInput,
|
||||
@@ -88,6 +94,7 @@ export type TSecretSync =
|
||||
| TCamundaSync
|
||||
| TVercelSync
|
||||
| TWindmillSync
|
||||
| THCVaultSync
|
||||
| TTeamCitySync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
@@ -103,6 +110,7 @@ export type TSecretSyncWithCredentials =
|
||||
| TCamundaSyncWithCredentials
|
||||
| TVercelSyncWithCredentials
|
||||
| TWindmillSyncWithCredentials
|
||||
| THCVaultSyncWithCredentials
|
||||
| TTeamCitySyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
@@ -118,6 +126,7 @@ export type TSecretSyncInput =
|
||||
| TCamundaSyncInput
|
||||
| TVercelSyncInput
|
||||
| TWindmillSyncInput
|
||||
| THCVaultSyncInput
|
||||
| TTeamCitySyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
@@ -133,6 +142,7 @@ export type TSecretSyncListItem =
|
||||
| TCamundaSyncListItem
|
||||
| TVercelSyncListItem
|
||||
| TWindmillSyncListItem
|
||||
| THCVaultSyncListItem
|
||||
| TTeamCitySyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">;
|
||||
|
||||
@@ -338,7 +338,7 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag
|
||||
|
||||
parsedArguments.SetDefaults()
|
||||
|
||||
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, parsedArguments.IsRecursive, "", *parsedArguments.ShouldExpandSecretReferences)
|
||||
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, true, parsedArguments.IsRecursive, "", *parsedArguments.ShouldExpandSecretReferences)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/hashicorp-vault/available"
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/hashicorp-vault"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Hashicorp Vault Connections](/integrations/app-connections/hashicorp-vault) to learn how to obtain the required credentials.
|
||||
</Note>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/hashicorp-vault/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/hashicorp-vault/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/hashicorp-vault/connection-name/{connectionName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/hashicorp-vault"
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/hashicorp-vault/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Hashicorp Vault Connections](/integrations/app-connections/hashicorp-vault) to learn how to obtain the required credentials.
|
||||
</Note>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/hashicorp-vault"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/hashicorp-vault/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/hashicorp-vault/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/hashicorp-vault/sync-name/{syncName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Import Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/import-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/hashicorp-vault"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/remove-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/sync-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/hashicorp-vault/{syncId}"
|
||||
---
|
||||
@@ -45,6 +45,10 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
|
||||
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
|
||||

|
||||
|
||||
<Info>
|
||||
Currently, the following JWT signature algorithms are supported: RS256, RS512, HS256, and EdDSA
|
||||
</Info>
|
||||
|
||||
Once you've done that, press **Update** to complete the required configuration.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -44,7 +44,9 @@ Prerequisites:
|
||||
To configure OIDC via the custom endpoints, set the **Configuration Type** field to **Custom** and input the required endpoint fields.
|
||||

|
||||
|
||||
2.3. Optionally, you can define a whitelist of allowed email domains.
|
||||
2.3. Select the appropriate JWT signature algorithm for your IdP. Currently, the supported options are RS256, RS512, HS256, and EdDSA.
|
||||
|
||||
2.4. Optionally, you can define a whitelist of allowed email domains.
|
||||
|
||||
Finally, fill out the **Client ID** and **Client Secret** fields and press **Update** to complete the required configuration.
|
||||
|
||||
|
||||
@@ -72,6 +72,10 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**.
|
||||

|
||||
|
||||
<Info>
|
||||
Currently, the following JWT signature algorithms are supported: RS256, RS512, HS256, and EdDSA
|
||||
</Info>
|
||||
|
||||
Once you've done that, press **Update** to complete the required configuration.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -35,6 +35,10 @@ Infisical supports these and many other identity providers:
|
||||
|
||||
If your required identity provider is not shown in the list above, please reach out to [[email protected]](mailto:[email protected]) for assistance.
|
||||
|
||||
<Info>
|
||||
For enhanced security, Infisical enforces PKCE (Proof Key for Code Exchange) with the OAuth 2.0-based SSO providers and OIDC. This provides additional protection against authorization code interception attacks and strengthens your authentication flow security.
|
||||
</Info>
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: "Microsoft Teams Integration"
|
||||
description: "Learn how to setup the Microsoft Teams integration"
|
||||
---
|
||||
|
||||
import MicrosoftTeamsWorkflowIntegration from '/snippets/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx';
|
||||
|
||||
|
||||
This guide will provide step by step instructions on how to configure Microsoft Teams integration for your Infisical projects.
|
||||
|
||||
## Setting up Microsoft Teams integration in your projects
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical Cloud">
|
||||
<MicrosoftTeamsWorkflowIntegration />
|
||||
</Tab>
|
||||
<Tab title="Self-hosted setup">
|
||||
### Configure Azure Resources
|
||||
To create a Microsoft Teams bot, you must first create an Azure Bot from the Azure Marketplace, an app registration, and a Microsoft Teams app. The steps below document in detail how to create and configure these resources.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Create Microsoft Teams app">
|
||||
Navigate to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/).
|
||||
|
||||
Once you're on the Microsoft Teams Developer Portal, press the "Create a new app" button on the overview page. Give the bot a name and press the "Add" button.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Create a Microsoft Teams bot">
|
||||
After creating the Microsoft Teams app, you'll need to create a Microsoft Teams bot.
|
||||
|
||||
Navigate to the app's bot settings page and click "Create a new bot".
|
||||
|
||||

|
||||

|
||||
|
||||
After clicking the "Create a new bot" button, you'll be navigated to the Teams Developer Portal for bot management. Press the "New bot" button, and enter the name of the bot.
|
||||
Please keep in mind that the name of the bot can only contain alphanumeric characters, dashes, and underscores.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Add a message endpoint to the bot">
|
||||
After creating the bot, you'll need to add a message endpoint to the bot. Navigate to the "Configure" tab, and input the following endpoint under "Endpoint address":
|
||||
`https://<your-infisical-instance-url>/api/v1/workflow-integrations/microsoft-teams/message-endpoint`
|
||||
Replace `<your-infisical-instance-url>` with the URL of your Infisical instance.
|
||||
|
||||
Press the "Save" button to save the changes.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Find the bot in Azure App Registrations">
|
||||
When you create a bot through the Teams Developer Portal, an Azure App Registration is also created.
|
||||
|
||||
Open your [Azure Portal](https://portal.azure.com/) and navigate to the "App Registrations" section to find the newly created app registration.
|
||||
The name of the app registration will be the same as the name of the bot you created in the previous step.
|
||||
|
||||
|
||||
Press the app registration to open the app registration overview page.
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Add API Permissions to the App Registration">
|
||||
Navigate to the "API Permissions" section of the app registration, and add the following permissions:
|
||||
- `AppCatalog.Read.All`
|
||||
- `ChannelSettings.Read.All`
|
||||
- `MultiTenantOrganization.Read.All`
|
||||
- `Organization.Read.All`
|
||||
- `Team.ReadBasic.All`
|
||||
- `TeamsAppInstallation.Read.All`
|
||||
|
||||
After adding the API permissions, press the "Grant admin consent" button to grant the permissions.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Add a new web application to the App Registration">
|
||||
Navigate to the "Authentication" section of the App Registration, and press the "Add a platform" button. Select the "Web" platform and enter the following redirect URI:
|
||||
`https://<your-infisical-instance-url>/organization/settings/oauth/callback`. Replace `<your-infisical-instance-url>` with the URL of your Infisical instance.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Get App Registration Client ID and Client Secret">
|
||||
Next we need to get the application client ID and create a new client secret. **Save these values for later, as they're required to configure the Microsoft Teams integration in Infisical.**
|
||||
|
||||
**Get the Application (Client) ID**
|
||||
|
||||
To get the Application (Client) ID, press the "Copy" button next to the "Application (client) ID" field.
|
||||
|
||||

|
||||
|
||||
**Create a new client secret**
|
||||
|
||||
Create a new client secret within the app registration. Navigate to the "Certificates & Secrets" section of the app registration, and press the "New client secret" button.
|
||||
|
||||

|
||||
|
||||
<Warning>
|
||||
Remember to rotate your client secret before it expires. Consider setting up a reminder or automated process to replace the secret and update your Infisical configuration before expiration.
|
||||
</Warning>
|
||||
</Step>
|
||||
|
||||
<Step title="Get the Microsoft Teams App ID">
|
||||
Navigate back to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), and press the "Apps" tab and select the app you created earlier.
|
||||
Here you can find the Microsoft Teams App ID in the overview page, which you need to copy and save for later.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Link the Microsoft Teams App to the bot">
|
||||
You need to link the Microsoft Teams App with the bot/app registration you created earlier.
|
||||
Inside the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), navigate to the "Bot" tab and select the bot you created earlier. Navigate to the "App Features" section, and press the "Bot" button.
|
||||
|
||||
Under the "What can your bot do?" section, enable `Only send notifications (one-way conversations)`.
|
||||
|
||||
Under the "Select the scopes where people can use your bot" section, select `Personal`, `Team`, and `Group Chat`.
|
||||
|
||||
Finally, press the "Save" button to save the changes.
|
||||
|
||||

|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Run an app validation test (Recommended)">
|
||||
To ensure that the Microsoft Teams App is working correctly, you can run an app validation test. This step is optional, but recommended to ensure the app is working correctly.
|
||||
|
||||
You should expect to see two errors related to sending welcome messages, because we haven't configured the Microsoft Teams App inside Infisical yet, which is required for proactive messages.
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
You may see manifest validation errors. Before running an app validation test, you must ensure that your app has all errors resolved, such as having a description and a valid name.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
If you see two errors for bot welcome messages, you can ignore them. This is expected until you configure the Microsoft Teams App inside Infisical.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Download the App Package">
|
||||
Once the Microsoft Teams App is working correctly, you can download the app package by navigating to the "Publish to Store" page, and pressing the "Download app package" button.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Configure Microsoft Teams Bot in Infisical
|
||||
|
||||
After creating the Microsoft Teams App and Bot, you are ready to configure the Microsoft Teams integration in Infisical.
|
||||
Please note that you must be an instance admin in order to configure the Microsoft Teams instance-wide settings.
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the Integrations tab in your Server Admin Console settings">
|
||||

|
||||

|
||||
|
||||
Enter the values you saved from the earlier steps into the respective fields.
|
||||
|
||||
- **Application (Client) ID**: The Client ID of the App Registration from the previous steps.
|
||||
- **Client Secret**: The Client Secret of the App Registration from the previous steps.
|
||||
- **Microsoft Teams App ID**: The App ID of the Microsoft Teams App from the previous steps.
|
||||
|
||||
Once completed, press the "Save" button to save your changes.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
<MicrosoftTeamsWorkflowIntegration />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<Accordion title="Notifications are not being sent to Microsoft Teams even though the integration is configured">
|
||||
If you recently added the Microsoft Teams app to your tenant, **it may take up to 24 hours for Microsoft Teams to propagate the changes.**
|
||||
A common indication of propagation issues is that the workflow integration is shown as "Installed", and you're able to view the teams and channels when configuring the workflow integration on your project, but no notification is being sent.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workflow Integration is stuck on 'Pending' status">
|
||||
The workflow integration can get stuck on Pending if you created the workflow integration before the Infisical Microsoft Teams bot was installed in the tenant.
|
||||
To resolve this, make sure you have installed the Infisical Microsoft Teams app in your tenant.
|
||||
|
||||
You can manually recheck the installation status by pressing the "Check Installation Status" button in the workflow organization settings.
|
||||
|
||||

|
||||
</Accordion>
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Slack integration"
|
||||
description: "Learn how to setup Slack integration"
|
||||
title: "Slack Integration"
|
||||
description: "Learn how to setup the Slack integration"
|
||||
---
|
||||
|
||||
This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
|
||||
|
||||
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 495 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 643 KiB |
|
After Width: | Height: | Size: 726 KiB |
|
After Width: | Height: | Size: 1.1 MiB |