diff --git a/backend/package-lock.json b/backend/package-lock.json index 0fc41d3e3..6ea34f692 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -81,7 +81,7 @@ "pino": "^8.16.2", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", - "probot": "^13.0.0", + "probot": "^13.3.8", "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", @@ -8018,6 +8018,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -8336,7 +8337,8 @@ "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.7", @@ -8814,9 +8816,10 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -8826,7 +8829,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -8840,6 +8843,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -8848,6 +8852,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -8858,7 +8863,8 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/bottleneck": { "version": "2.19.5", @@ -9006,6 +9012,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -9028,13 +9035,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9379,6 +9392,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -9543,16 +9557,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -9618,6 +9636,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -9724,7 +9743,8 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.816", @@ -9738,9 +9758,10 @@ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -9827,6 +9848,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", @@ -10452,6 +10494,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10495,36 +10538,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -10588,6 +10632,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10595,12 +10640,14 @@ "node_modules/express/node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -10608,7 +10655,8 @@ "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/extend": { "version": "3.0.2", @@ -10815,12 +10863,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10835,6 +10884,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -10842,7 +10892,8 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/find-my-way": { "version": "8.1.0", @@ -11008,6 +11059,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -11365,15 +11417,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11719,11 +11776,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13276,6 +13334,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -13286,9 +13345,13 @@ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -13309,6 +13372,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -13748,6 +13812,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -14099,6 +14164,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -14511,9 +14577,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -14716,20 +14783,78 @@ } }, "node_modules/pino-http": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.6.1.tgz", - "integrity": "sha512-J0hiJgUExtBXP2BjrK4VB305tHXS31sCmWJ9XJo2wPkLHa1NFPuW4V9wjG27PAc2fmBCigiNhQKpvrx+kntBPA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.3.0.tgz", + "integrity": "sha512-kaHQqt1i5S9LXWmyuw6aPPqYW/TjoDPizPs4PnDW4hSpajz2Uo/oisNliLf7We1xzpiLacdntmw8yaZiEkppQQ==", + "license": "MIT", "dependencies": { "get-caller-file": "^2.0.5", - "pino": "^8.17.1", - "pino-std-serializers": "^6.2.2", - "process-warning": "^3.0.0" + "pino": "^9.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0" } }, + "node_modules/pino-http/node_modules/pino": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz", + "integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-http/node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-http/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, "node_modules/pino-http/node_modules/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" + }, + "node_modules/pino-http/node_modules/sonic-boom": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz", + "integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/pino-http/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } }, "node_modules/pino-pretty": { "version": "10.2.3", @@ -15096,9 +15221,10 @@ } }, "node_modules/probot": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/probot/-/probot-13.0.0.tgz", - "integrity": "sha512-3ht9kAJ+ISjLyWLLCKVdrLE5xs/x+zUx07J5kYTxAyIxUvwF6Acr8xT5fiNihbBHAsEl4+A4CMYZQvZ5hx5bgw==", + "version": "13.3.8", + "resolved": "https://registry.npmjs.org/probot/-/probot-13.3.8.tgz", + "integrity": "sha512-xc+KBC0mp1JKFMsPbMyj1SpmN0B7Q8uFO7ze4PBbNv74q8AyPGqYL3TmkZSOmcOjFTeFrZTnMYEoXi+z1anyLA==", + "license": "ISC", "dependencies": { "@octokit/core": "^5.0.2", "@octokit/plugin-enterprise-compatibility": "^4.0.1", @@ -15113,19 +15239,18 @@ "@probot/octokit-plugin-config": "^2.0.1", "@probot/pino": "^2.3.5", "@types/express": "^4.17.21", - "commander": "^11.1.0", + "bottleneck": "^2.19.5", + "commander": "^12.0.0", "deepmerge": "^4.3.1", "dotenv": "^16.3.1", - "eventsource": "^2.0.2", - "express": "^4.18.2", + "express": "^4.21.0", "ioredis": "^5.3.2", "js-yaml": "^4.1.0", "lru-cache": "^10.0.3", "octokit-auth-probot": "^2.0.0", - "pino": "^8.16.1", - "pino-http": "^8.5.1", + "pino": "^9.0.0", + "pino-http": "^10.0.0", "pkg-conf": "^3.1.0", - "resolve": "^1.22.8", "update-dotenv": "^1.1.1" }, "bin": { @@ -15152,11 +15277,12 @@ } }, "node_modules/probot/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/probot/node_modules/lru-cache": { @@ -15167,6 +15293,68 @@ "node": "14 || >=16.14" } }, + "node_modules/probot/node_modules/pino": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz", + "integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/probot/node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/probot/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/probot/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" + }, + "node_modules/probot/node_modules/sonic-boom": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz", + "integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/probot/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -15282,11 +15470,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -15359,6 +15548,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -15367,6 +15557,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -15381,6 +15572,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -15961,9 +16153,10 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15987,6 +16180,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -15994,12 +16188,23 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -16013,14 +16218,15 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -16037,14 +16243,17 @@ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16103,13 +16312,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17704,6 +17918,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -17927,6 +18142,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -18051,6 +18267,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } diff --git a/backend/package.json b/backend/package.json index c7aafcbf9..21f841da7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -178,7 +178,7 @@ "pino": "^8.16.2", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", - "probot": "^13.0.0", + "probot": "^13.3.8", "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index 052263835..4b1566c55 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -77,6 +77,39 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => } }); + server.route({ + method: "POST", + url: "/entra-id/users", + config: { + rateLimit: readLimit + }, + schema: { + body: z.object({ + tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"), + applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"), + clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration") + }), + response: { + 200: z + .object({ + name: z.string().min(1).describe("The name of the user"), + id: z.string().min(1).describe("The ID of the user"), + email: z.string().min(1).describe("The email of the user") + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({ + tenantId: req.body.tenantId, + applicationId: req.body.applicationId, + clientSecret: req.body.clientSecret + }); + return data; + } + }); + server.route({ method: "PATCH", url: "/:name", diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 401ebf1ef..3f6492571 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -23,6 +23,7 @@ import { TListDynamicSecretsMultiEnvDTO, TUpdateDynamicSecretDTO } from "./dynamic-secret-types"; +import { AzureEntraIDProvider } from "./providers/azure-entra-id"; import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models"; type TDynamicSecretServiceFactoryDep = { @@ -462,6 +463,23 @@ export const dynamicSecretServiceFactory = ({ return dynamicSecretCfg; }; + const fetchAzureEntraIdUsers = async ({ + tenantId, + applicationId, + clientSecret + }: { + tenantId: string; + applicationId: string; + clientSecret: string; + }) => { + const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers( + tenantId, + applicationId, + clientSecret + ); + return azureEntraIdUsers; + }; + return { create, updateByName, @@ -470,6 +488,7 @@ export const dynamicSecretServiceFactory = ({ listDynamicSecretsByEnv, listDynamicSecretsByFolderIds, getDynamicSecretCount, - getCountMultiEnv + getCountMultiEnv, + fetchAzureEntraIdUsers }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts new file mode 100644 index 000000000..e2dfe2d4b --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts @@ -0,0 +1,138 @@ +import axios from "axios"; +import { customAlphabet } from "nanoid"; + +import { BadRequestError } from "@app/lib/errors"; + +import { AzureEntraIDSchema, TDynamicProviderFns } from "./models"; + +const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/"; +const MSFT_LOGIN_URL = "https://login.microsoftonline.com"; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +type User = { name: string; id: string; email: string }; + +export const AzureEntraIDProvider = (): TDynamicProviderFns & { + fetchAzureEntraIdUsers: (tenantId: string, applicationId: string, clientSecret: string) => Promise; +} => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await AzureEntraIDSchema.parseAsync(inputs); + return providerInputs; + }; + + const getToken = async ( + tenantId: string, + applicationId: string, + clientSecret: string + ): Promise<{ token?: string; success: boolean }> => { + const response = await axios.post<{ access_token: string }>( + `${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`, + { + grant_type: "client_credentials", + client_id: applicationId, + client_secret: clientSecret, + scope: "https://graph.microsoft.com/.default" + }, + { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + if (response.status === 200) { + return { token: response.data.access_token, success: true }; + } + return { success: false }; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + return data.success; + }; + + const renew = async (inputs: unknown, entityId: string) => { + // Do nothing + return { entityId }; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + if (!data.success) { + throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); + } + + const password = generatePassword(); + + const response = await axios.patch( + `${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`, + { + passwordProfile: { + forceChangePasswordNextSignIn: false, + password + } + }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${data.token}` + } + } + ); + if (response.status !== 204) { + throw new BadRequestError({ message: "Failed to update password" }); + } + + return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } }; + }; + + const revoke = async (inputs: unknown, entityId: string) => { + // Creates a new password + await create(inputs); + return { entityId }; + }; + + const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => { + const data = await getToken(tenantId, applicationId, clientSecret); + if (!data.success) { + throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); + } + + const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>( + `${MSFT_GRAPH_API_URL}/users`, + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Bearer ${data.token}` + } + } + ); + + if (response.status !== 200) { + throw new BadRequestError({ message: "Failed to fetch users" }); + } + + const users = response.data.value.map((user) => { + return { + name: user.displayName, + id: user.id, + email: user.userPrincipalName + }; + }); + return users; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew, + fetchAzureEntraIdUsers + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 6ae22c869..8f2dbdc13 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,5 +1,6 @@ import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; +import { AzureEntraIDProvider } from "./azure-entra-id"; import { CassandraProvider } from "./cassandra"; import { ElasticSearchProvider } from "./elastic-search"; import { DynamicSecretProviders } from "./models"; @@ -18,5 +19,6 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(), [DynamicSecretProviders.MongoDB]: MongoDBProvider(), [DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(), - [DynamicSecretProviders.RabbitMq]: RabbitMqProvider() + [DynamicSecretProviders.RabbitMq]: RabbitMqProvider(), + [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index f23a60df7..18a7b3dc9 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -166,6 +166,14 @@ export const DynamicSecretMongoDBSchema = z.object({ ) }); +export const AzureEntraIDSchema = z.object({ + tenantId: z.string().trim().min(1), + userId: z.string().trim().min(1), + email: z.string().trim().min(1), + applicationId: z.string().trim().min(1), + clientSecret: z.string().trim().min(1) +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -175,7 +183,8 @@ export enum DynamicSecretProviders { MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", MongoDB = "mongo-db", - RabbitMq = "rabbit-mq" + RabbitMq = "rabbit-mq", + AzureEntraID = "azure-entra-id" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -187,7 +196,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }), z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), - z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }) + z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), + z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index c4a6ac3fc..64ca8ad41 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -1,7 +1,5 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; -import { conditionsMatcher } from "@app/lib/casl"; - export enum OrgPermissionActions { Read = "read", Create = "create", @@ -48,7 +46,7 @@ export type OrgPermissionSet = | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; const buildAdminPermission = () => { - const { can, build } = new AbilityBuilder>(createMongoAbility); + const { can, rules } = new AbilityBuilder>(createMongoAbility); // ws permissions can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); @@ -115,13 +113,13 @@ const buildAdminPermission = () => { can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); - return build({ conditionsMatcher }); + return rules; }; export const orgAdminPermissions = buildAdminPermission(); const buildMemberPermission = () => { - const { can, build } = new AbilityBuilder>(createMongoAbility); + const { can, rules } = new AbilityBuilder>(createMongoAbility); can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); @@ -142,14 +140,14 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); - return build({ conditionsMatcher }); + return rules; }; export const orgMemberPermissions = buildMemberPermission(); const buildNoAccessPermission = () => { - const { build } = new AbilityBuilder>(createMongoAbility); - return build({ conditionsMatcher }); + const { rules } = new AbilityBuilder>(createMongoAbility); + return rules; }; export const orgNoAccessPermissions = buildNoAccessPermission(); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index cd392d3c7..70a06739e 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -1,7 +1,13 @@ import { z } from "zod"; import { TDbClient } from "@app/db"; -import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas"; +import { + IdentityProjectMembershipRoleSchema, + OrgMembershipsSchema, + TableName, + TProjectRoles, + TProjects +} from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; @@ -10,18 +16,91 @@ export type TPermissionDALFactory = ReturnType; export const permissionDALFactory = (db: TDbClient) => { const getOrgPermission = async (userId: string, orgId: string) => { try { + const groupSubQuery = db(TableName.Groups) + .where(`${TableName.Groups}.orgId`, orgId) + .join(TableName.UserGroupMembership, (queryBuilder) => { + queryBuilder + .on(`${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .andOn(`${TableName.UserGroupMembership}.userId`, db.raw("?", [userId])); + }) + .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) + .select( + db.ref("id").withSchema(TableName.Groups).as("groupId"), + db.ref("orgId").withSchema(TableName.Groups).as("groupOrgId"), + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), + db.ref("role").withSchema(TableName.Groups).as("groupRole"), + db.ref("roleId").withSchema(TableName.Groups).as("groupRoleId"), + db.ref("createdAt").withSchema(TableName.Groups).as("groupCreatedAt"), + db.ref("updatedAt").withSchema(TableName.Groups).as("groupUpdatedAt"), + db.ref("permissions").withSchema(TableName.OrgRoles).as("groupCustomRolePermission") + ); + const membership = await db .replicaNode()(TableName.OrgMembership) - .leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`) - .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) - .where("userId", userId) .where(`${TableName.OrgMembership}.orgId`, orgId) - .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) - .select("permissions") - .select(selectAllTableCols(TableName.OrgMembership)) - .first(); + .where(`${TableName.OrgMembership}.userId`, userId) + .leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`) + .leftJoin[0]>( + groupSubQuery.as("userGroups"), + "userGroups.groupOrgId", + db.raw("?", [orgId]) + ) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .select( + selectAllTableCols(TableName.OrgMembership), + db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), + db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("groupId").withSchema("userGroups"), + db.ref("groupOrgId").withSchema("userGroups"), + db.ref("groupName").withSchema("userGroups"), + db.ref("groupSlug").withSchema("userGroups"), + db.ref("groupRole").withSchema("userGroups"), + db.ref("groupRoleId").withSchema("userGroups"), + db.ref("groupCreatedAt").withSchema("userGroups"), + db.ref("groupUpdatedAt").withSchema("userGroups"), + db.ref("groupCustomRolePermission").withSchema("userGroups") + ); - return membership; + const [formatedDoc] = sqlNestRelationships({ + data: membership, + key: "id", + parentMapper: (el) => + OrgMembershipsSchema.extend({ + permissions: z.unknown(), + orgAuthEnforced: z.boolean().optional().nullable(), + customRoleSlug: z.string().optional().nullable() + }).parse(el), + childrenMapper: [ + { + key: "groupId", + label: "groups" as const, + mapper: ({ + groupId, + groupUpdatedAt, + groupCreatedAt, + groupRole, + groupRoleId, + groupCustomRolePermission, + groupName, + groupSlug, + groupOrgId + }) => ({ + id: groupId, + updatedAt: groupUpdatedAt, + createdAt: groupCreatedAt, + role: groupRole, + roleId: groupRoleId, + customRolePermission: groupCustomRolePermission, + name: groupName, + slug: groupSlug, + orgId: groupOrgId + }) + } + ] + }); + + return formatedDoc; } catch (error) { throw new DatabaseError({ error, name: "GetOrgPermission" }); } @@ -47,74 +126,31 @@ export const permissionDALFactory = (db: TDbClient) => { const getProjectPermission = async (userId: string, projectId: string) => { try { - const groups: string[] = await db - .replicaNode()(TableName.GroupProjectMembership) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) - .pluck(`${TableName.GroupProjectMembership}.groupId`); - - const groupDocs = await db - .replicaNode()(TableName.UserGroupMembership) - .where(`${TableName.UserGroupMembership}.userId`, userId) - .whereIn(`${TableName.UserGroupMembership}.groupId`, groups) - .join( - TableName.GroupProjectMembership, - `${TableName.GroupProjectMembership}.groupId`, - `${TableName.UserGroupMembership}.groupId` - ) - .join( + const docs = await db + .replicaNode()(TableName.Users) + .where(`${TableName.Users}.id`, userId) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) + .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { + void queryBuilder + .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) + .andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`); + }) + .leftJoin( TableName.GroupProjectMembershipRole, `${TableName.GroupProjectMembershipRole}.projectMembershipId`, `${TableName.GroupProjectMembership}.id` ) - - .leftJoin( - TableName.ProjectRoles, + .leftJoin( + { groupCustomRoles: TableName.ProjectRoles }, `${TableName.GroupProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` + `groupCustomRoles.id` ) - .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) - + .leftJoin(TableName.ProjectMembership, (queryBuilder) => { + void queryBuilder + .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) + .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); + }) .leftJoin( - TableName.ProjectUserAdditionalPrivilege, - `${TableName.GroupProjectMembership}.projectId`, - `${TableName.Project}.id` - ) - .select(selectAllTableCols(TableName.GroupProjectMembershipRole)) - .select( - db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"), - db.ref("projectId").withSchema(TableName.GroupProjectMembership), - db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), - db.ref("orgId").withSchema(TableName.Project), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - - db.ref("permissions").withSchema(TableName.ProjectRoles).as("permissions"), - // db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("apPermissions") - // Additional Privileges - db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"), - db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"), - db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"), - db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"), - - db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"), - - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userApTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userApTemporaryAccessEndTime") - ); - // .select(`${TableName.ProjectRoles}.permissions`); - - const docs = await db(TableName.ProjectMembership) - .join( TableName.ProjectUserMembershipRole, `${TableName.ProjectUserMembershipRole}.projectMembershipId`, `${TableName.ProjectMembership}.id` @@ -124,176 +160,229 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.ProjectUserMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) - .leftJoin( - TableName.ProjectUserAdditionalPrivilege, - `${TableName.ProjectUserAdditionalPrivilege}.projectId`, - `${TableName.ProjectMembership}.projectId` - ) - - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { + void queryBuilder + .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) + .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); + }) + .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) - .where(`${TableName.ProjectMembership}.userId`, userId) - .where(`${TableName.ProjectMembership}.projectId`, projectId) - .select(selectAllTableCols(TableName.ProjectUserMembershipRole)) .select( + db.ref("id").withSchema(TableName.Users).as("userId"), + // groups specific + db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), + db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"), + db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"), + db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"), + db + .ref("customRoleId") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleCustomRoleId"), + db + .ref("isTemporary") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleIsTemporary"), + db + .ref("temporaryMode") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryMode"), + db + .ref("temporaryRange") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryAccessEndTime"), + // user specific db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), - db.ref("projectId").withSchema(TableName.ProjectMembership), - db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), - db.ref("orgId").withSchema(TableName.Project), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles), - db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"), - db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"), - db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"), - db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"), - - db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"), - + db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"), + db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"), + db + .ref("temporaryMode") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryMode"), + db + .ref("isTemporary") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryAccessEndTime"), + db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"), + db + .ref("permissions") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesPermissions"), + db + .ref("temporaryMode") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryMode"), + db + .ref("isTemporary") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryRange"), + db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"), db .ref("temporaryAccessStartTime") .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userApTemporaryAccessStartTime"), + .as("userAdditionalPrivilegesTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userApTemporaryAccessEndTime") + .as("userAdditionalPrivilegesTemporaryAccessEndTime"), + // general + db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("orgId").withSchema(TableName.Project), + db.ref("id").withSchema(TableName.Project).as("projectId") ); - const permission = sqlNestRelationships({ + const [userPermission] = sqlNestRelationships({ data: docs, key: "projectId", - parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({ + parentMapper: ({ + orgId, + orgAuthEnforced, + membershipId, + groupMembershipId, + membershipCreatedAt, + groupMembershipCreatedAt, + groupMembershipUpdatedAt, + membershipUpdatedAt + }) => ({ orgId, orgAuthEnforced, userId, - id: membershipId, projectId, - createdAt: membershipCreatedAt, - updatedAt: membershipUpdatedAt + id: membershipId || groupMembershipId, + createdAt: membershipCreatedAt || groupMembershipCreatedAt, + updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt }), childrenMapper: [ { - key: "id", - label: "roles" as const, - mapper: (data) => - ProjectUserMembershipRolesSchema.extend({ - permissions: z.unknown(), - customRoleSlug: z.string().optional().nullable() - }).parse(data) + key: "userGroupProjectMembershipRoleId", + label: "userGroupRoles" as const, + mapper: ({ + userGroupProjectMembershipRoleId, + userGroupProjectMembershipRole, + userGroupProjectMembershipRolePermission, + userGroupProjectMembershipRoleCustomRoleSlug, + userGroupProjectMembershipRoleIsTemporary, + userGroupProjectMembershipRoleTemporaryMode, + userGroupProjectMembershipRoleTemporaryAccessEndTime, + userGroupProjectMembershipRoleTemporaryAccessStartTime, + userGroupProjectMembershipRoleTemporaryRange + }) => ({ + id: userGroupProjectMembershipRoleId, + role: userGroupProjectMembershipRole, + customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug, + permissions: userGroupProjectMembershipRolePermission, + temporaryRange: userGroupProjectMembershipRoleTemporaryRange, + temporaryMode: userGroupProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: userGroupProjectMembershipRoleIsTemporary + }) }, { - key: "userApId", + key: "userProjectMembershipRoleId", + label: "projecMembershiptRoles" as const, + mapper: ({ + userProjectMembershipRoleId, + userProjectMembershipRole, + userProjectCustomRolePermission, + userProjectMembershipRoleIsTemporary, + userProjectMembershipRoleTemporaryMode, + userProjectMembershipRoleTemporaryRange, + userProjectMembershipRoleTemporaryAccessEndTime, + userProjectMembershipRoleTemporaryAccessStartTime, + userProjectMembershipRoleCustomRoleSlug + }) => ({ + id: userProjectMembershipRoleId, + role: userProjectMembershipRole, + customRoleSlug: userProjectMembershipRoleCustomRoleSlug, + permissions: userProjectCustomRolePermission, + temporaryRange: userProjectMembershipRoleTemporaryRange, + temporaryMode: userProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: userProjectMembershipRoleIsTemporary + }) + }, + { + key: "userAdditionalPrivilegesId", label: "additionalPrivileges" as const, mapper: ({ - userApId, - userApPermissions, - userApIsTemporary, - userApTemporaryMode, - userApTemporaryRange, - userApTemporaryAccessEndTime, - userApTemporaryAccessStartTime + userAdditionalPrivilegesId, + userAdditionalPrivilegesPermissions, + userAdditionalPrivilegesIsTemporary, + userAdditionalPrivilegesTemporaryMode, + userAdditionalPrivilegesTemporaryRange, + userAdditionalPrivilegesTemporaryAccessEndTime, + userAdditionalPrivilegesTemporaryAccessStartTime }) => ({ - id: userApId, - permissions: userApPermissions, - temporaryRange: userApTemporaryRange, - temporaryMode: userApTemporaryMode, - temporaryAccessEndTime: userApTemporaryAccessEndTime, - temporaryAccessStartTime: userApTemporaryAccessStartTime, - isTemporary: userApIsTemporary + id: userAdditionalPrivilegesId, + permissions: userAdditionalPrivilegesPermissions, + temporaryRange: userAdditionalPrivilegesTemporaryRange, + temporaryMode: userAdditionalPrivilegesTemporaryMode, + temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, + temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, + isTemporary: userAdditionalPrivilegesIsTemporary }) } ] }); - const groupPermission = groupDocs.length - ? sqlNestRelationships({ - data: groupDocs, - key: "projectId", - parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({ - orgId, - orgAuthEnforced, - userId, - id: membershipId, - projectId, - createdAt: membershipCreatedAt, - updatedAt: membershipUpdatedAt - }), - childrenMapper: [ - { - key: "id", - label: "roles" as const, - mapper: (data) => - ProjectUserMembershipRolesSchema.extend({ - permissions: z.unknown(), - customRoleSlug: z.string().optional().nullable() - }).parse(data) - }, - { - key: "userApId", - label: "additionalPrivileges" as const, - mapper: ({ - userApId, - userApProjectId, - userApUserId, - userApPermissions, - userApIsTemporary, - userApTemporaryMode, - userApTemporaryRange, - userApTemporaryAccessEndTime, - userApTemporaryAccessStartTime - }) => ({ - id: userApId, - userId: userApUserId, - projectId: userApProjectId, - permissions: userApPermissions, - temporaryRange: userApTemporaryRange, - temporaryMode: userApTemporaryMode, - temporaryAccessEndTime: userApTemporaryAccessEndTime, - temporaryAccessStartTime: userApTemporaryAccessStartTime, - isTemporary: userApIsTemporary - }) - } - ] - }) - : []; - - if (!permission?.[0] && !groupPermission[0]) return undefined; + if (!userPermission) return undefined; + if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined; // when introducting cron mode change it here const activeRoles = - permission?.[0]?.roles?.filter( + userPermission?.projecMembershiptRoles?.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ) ?? []; const activeGroupRoles = - groupPermission?.[0]?.roles?.filter( + userPermission?.userGroupRoles?.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ) ?? []; const activeAdditionalPrivileges = - permission?.[0]?.additionalPrivileges?.filter( + userPermission?.additionalPrivileges?.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ) ?? []; - const activeGroupAdditionalPrivileges = - groupPermission?.[0]?.additionalPrivileges?.filter( - ({ isTemporary, temporaryAccessEndTime, userId: apUserId, projectId: apProjectId }) => - apProjectId === projectId && - apUserId === userId && - (!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)) - ) ?? []; - return { - ...(permission[0] || groupPermission[0]), + ...userPermission, roles: [...activeRoles, ...activeGroupRoles], - additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges] + additionalPrivileges: activeAdditionalPrivileges }; } catch (error) { throw new DatabaseError({ error, name: "GetProjectPermission" }); diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 72a35a326..d96b8a978 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -20,7 +20,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; import { validateOrgSAML } from "./permission-fns"; -import { TBuildProjectPermissionDTO } from "./permission-types"; +import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types"; import { buildServiceTokenProjectPermission, projectAdminPermissions, @@ -47,26 +47,29 @@ export const permissionServiceFactory = ({ serviceTokenDAL, projectDAL }: TPermissionServiceFactoryDep) => { - const buildOrgPermission = (role: string, permission?: unknown) => { - switch (role) { - case OrgMembershipRole.Admin: - return orgAdminPermissions; - case OrgMembershipRole.Member: - return orgMemberPermissions; - case OrgMembershipRole.NoAccess: - return orgNoAccessPermissions; - case OrgMembershipRole.Custom: - return createMongoAbility( - unpackRules>>( - permission as PackRule>>[] - ), - { - conditionsMatcher - } - ); - default: - throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" }); - } + const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { + const rules = orgUserRoles + .map(({ role, permissions }) => { + switch (role) { + case OrgMembershipRole.Admin: + return orgAdminPermissions; + case OrgMembershipRole.Member: + return orgMemberPermissions; + case OrgMembershipRole.NoAccess: + return orgNoAccessPermissions; + case OrgMembershipRole.Custom: + return unpackRules>>( + permissions as PackRule>>[] + ); + default: + throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" }); + } + }) + .reduce((curr, prev) => prev.concat(curr), []); + + return createMongoAbility(rules, { + conditionsMatcher + }); }; const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => { @@ -129,7 +132,13 @@ export const permissionServiceFactory = ({ validateOrgSAML(authMethod, membership.orgAuthEnforced); - return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; + const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( + membership?.groups?.map(({ role, customRolePermission }) => ({ + role, + permissions: customRolePermission + })) || [] + ); + return { permission: buildOrgPermission(finalPolicyRoles), membership }; }; const getIdentityOrgPermission = async (identityId: string, orgId: string) => { @@ -138,7 +147,10 @@ export const permissionServiceFactory = ({ if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } - return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; + return { + permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]), + membership + }; }; const getOrgPermission = async ( @@ -169,11 +181,11 @@ export const permissionServiceFactory = ({ const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); if (!orgRole) throw new BadRequestError({ message: "Role not found" }); return { - permission: buildOrgPermission(OrgMembershipRole.Custom, orgRole.permissions), + permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]), role: orgRole }; } - return { permission: buildOrgPermission(role, []) }; + return { permission: buildOrgPermission([{ role, permissions: [] }]) }; }; // user permission for a project in an organization diff --git a/backend/src/ee/services/permission/permission-types.ts b/backend/src/ee/services/permission/permission-types.ts index a35958ffd..620e7a61c 100644 --- a/backend/src/ee/services/permission/permission-types.ts +++ b/backend/src/ee/services/permission/permission-types.ts @@ -2,3 +2,8 @@ export type TBuildProjectPermissionDTO = { permissions?: unknown; role: string; }[]; + +export type TBuildOrgPermissionDTO = { + permissions?: unknown; + role: string; +}[]; diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 68fcb0db2..0606f9dba 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -7,7 +7,11 @@ import { TScanFullRepoEventPayload, TScanPushEventPayload } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; -import { TSyncSecretsDTO } from "@app/services/secret/secret-types"; +import { + TFailedIntegrationSyncEmailsPayload, + TIntegrationSyncPayload, + TSyncSecretsDTO +} from "@app/services/secret/secret-types"; export enum QueueName { SecretRotation = "secret-rotation", @@ -42,6 +46,7 @@ export enum QueueJobs { SecWebhook = "secret-webhook-trigger", TelemetryInstanceStats = "telemetry-self-hosted-stats", IntegrationSync = "secret-integration-pull", + SendFailedIntegrationSyncEmails = "send-failed-integration-sync-emails", SecretScan = "secret-scan", UpgradeProjectToGhost = "upgrade-project-to-ghost-job", DynamicSecretRevocation = "dynamic-secret-revocation", @@ -88,18 +93,26 @@ export type TQueueJobTypes = { name: QueueJobs.SecWebhook; payload: { projectId: string; environment: string; secretPath: string; depth?: number }; }; - [QueueName.IntegrationSync]: { - name: QueueJobs.IntegrationSync; - payload: { - isManual?: boolean; - actorId?: string; - projectId: string; - environment: string; - secretPath: string; - depth?: number; - deDupeQueue?: Record; - }; - }; + + [QueueName.AccessTokenStatusUpdate]: + | { + name: QueueJobs.IdentityAccessTokenStatusUpdate; + payload: { identityAccessTokenId: string; numberOfUses: number }; + } + | { + name: QueueJobs.ServiceTokenStatusUpdate; + payload: { serviceTokenId: string }; + }; + + [QueueName.IntegrationSync]: + | { + name: QueueJobs.IntegrationSync; + payload: TIntegrationSyncPayload; + } + | { + name: QueueJobs.SendFailedIntegrationSyncEmails; + payload: TFailedIntegrationSyncEmailsPayload; + }; [QueueName.SecretFullRepoScan]: { name: QueueJobs.SecretScan; payload: TScanFullRepoEventPayload; @@ -153,15 +166,6 @@ export type TQueueJobTypes = { name: QueueJobs.ProjectV3Migration; payload: { projectId: string }; }; - [QueueName.AccessTokenStatusUpdate]: - | { - name: QueueJobs.IdentityAccessTokenStatusUpdate; - payload: { identityAccessTokenId: string; numberOfUses: number }; - } - | { - name: QueueJobs.ServiceTokenStatusUpdate; - payload: { serviceTokenId: string }; - }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 3320c7d87..1aa2c0a44 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; import fastifyPlugin from "fastify-plugin"; +import jwt from "jsonwebtoken"; import { ZodError } from "zod"; import { @@ -11,6 +12,12 @@ import { UnauthorizedError } from "@app/lib/errors"; +enum JWTErrors { + JwtExpired = "jwt expired", + JwtMalformed = "jwt malformed", + InvalidAlgorithm = "invalid algorithm" +} + export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => { server.setErrorHandler((error, req, res) => { req.log.error(error); @@ -36,6 +43,27 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider status: error.status, detail: error.detail }); + // Handle JWT errors and make them more human-readable for the end-user. + } else if (error instanceof jwt.JsonWebTokenError) { + const message = (() => { + if (error.message === JWTErrors.JwtExpired) { + return "Your token has expired. Please re-authenticate."; + } + if (error.message === JWTErrors.JwtMalformed) { + return "The provided access token is malformed. Please use a valid token or generate a new one and try again."; + } + if (error.message === JWTErrors.InvalidAlgorithm) { + return "The access token is signed with an invalid algorithm. Please provide a valid token and try again."; + } + + return error.message; + })(); + + void res.status(401).send({ + statusCode: 401, + error: "TokenError", + message + }); } else { void res.send(error); } diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 4c687e86c..a0feb824c 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -18,7 +18,7 @@ import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -68,12 +68,12 @@ export const identityOidcAuthServiceFactory = ({ identityId: identityOidcAuth.identityId }); if (!identityMembershipOrg) { - throw new BadRequestError({ message: "Failed to find identity" }); + throw new NotFoundError({ message: "Failed to find identity in organization" }); } const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); if (!orgBot) { - throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + throw new NotFoundError({ message: "Org bot not found", name: "OrgBotNotFound" }); } const key = infisicalSymmetricDecrypt({ @@ -106,7 +106,7 @@ export const identityOidcAuthServiceFactory = ({ const decodedToken = jwt.decode(oidcJwt, { complete: true }); if (!decodedToken) { - throw new BadRequestError({ + throw new UnauthorizedError({ message: "Invalid JWT" }); } @@ -119,13 +119,24 @@ export const identityOidcAuthServiceFactory = ({ const { kid } = decodedToken.header; const oidcSigningKey = await client.getSigningKey(kid); - const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), { - issuer: identityOidcAuth.boundIssuer - }) as Record; + let tokenData: Record; + try { + tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), { + issuer: identityOidcAuth.boundIssuer + }) as Record; + } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + throw new UnauthorizedError({ + message: `Access denied: ${error.message}` + }); + } + + throw error; + } if (identityOidcAuth.boundSubject) { if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) { - throw new ForbiddenRequestError({ + throw new UnauthorizedError({ message: "Access denied: OIDC subject not allowed." }); } @@ -137,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({ .split(", ") .some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue)) ) { - throw new ForbiddenRequestError({ + throw new UnauthorizedError({ message: "Access denied: OIDC audience not allowed." }); } @@ -150,7 +161,7 @@ export const identityOidcAuthServiceFactory = ({ if ( !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry)) ) { - throw new ForbiddenRequestError({ + throw new UnauthorizedError({ message: "Access denied: OIDC claim not allowed." }); } diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts index 26cfd67eb..d734d7818 100644 --- a/backend/src/services/org/org-role-service.ts +++ b/backend/src/services/org/org-role-service.ts @@ -60,7 +60,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol name: "Admin", slug: "admin", description: "Complete administration access over the organization", - permissions: packRules(orgAdminPermissions.rules), + permissions: packRules(orgAdminPermissions), createdAt: new Date(), updatedAt: new Date() }; @@ -72,7 +72,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol name: "Member", slug: "member", description: "Non-administrative role in an organization", - permissions: packRules(orgMemberPermissions.rules), + permissions: packRules(orgMemberPermissions), createdAt: new Date(), updatedAt: new Date() }; @@ -84,7 +84,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol name: "No Access", slug: "no-access", description: "No access to any resources in the organization", - permissions: packRules(orgNoAccessPermissions.rules), + permissions: packRules(orgNoAccessPermissions), createdAt: new Date(), updatedAt: new Date() }; @@ -151,7 +151,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol name: "Admin", slug: "admin", description: "Complete administration access over the organization", - permissions: packRules(orgAdminPermissions.rules), + permissions: packRules(orgAdminPermissions), createdAt: new Date(), updatedAt: new Date() }, @@ -161,7 +161,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol name: "Member", slug: "member", description: "Non-administrative role in an organization", - permissions: packRules(orgMemberPermissions.rules), + permissions: packRules(orgMemberPermissions), createdAt: new Date(), updatedAt: new Date() }, @@ -171,7 +171,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol name: "No Access", slug: "no-access", description: "No access to any resources in the organization", - permissions: packRules(orgNoAccessPermissions.rules), + permissions: packRules(orgNoAccessPermissions), createdAt: new Date(), updatedAt: new Date() }, diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index 9cdb52cff..4efcbb34b 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -26,7 +26,10 @@ export const getBotKeyFnFactory = ( ) => { const getBotKeyFn = async (projectId: string) => { const project = await projectDAL.findById(projectId); - if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." }); + if (!project) + throw new BadRequestError({ + message: "Project not found during bot lookup. Are you sure you are using the correct project ID?" + }); if (project.version === 3) { return { project, shouldUseSecretV2Bridge: true }; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index cbbfdc9ac..f4dd03bf3 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -545,7 +545,11 @@ export const secretImportServiceFactory = ({ return importedSecrets; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); return importedSecrets.map((el) => ({ diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 7837b716b..e77972d37 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -832,7 +832,11 @@ export const createManySecretsRawFnFactory = ({ secretDAL }); - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const inputSecrets = secrets.map((secret) => { const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); @@ -993,7 +997,11 @@ export const updateManySecretsRawFnFactory = ({ return updatedSecrets; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" }); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 07cb0923d..4005c8f7a 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1,7 +1,13 @@ /* eslint-disable no-await-in-loop */ import { AxiosError } from "axios"; -import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas"; +import { + ProjectMembershipRole, + ProjectUpgradeStatus, + ProjectVersion, + TSecretSnapshotSecretsV2, + TSecretVersionsV2 +} from "@app/db/schemas"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; @@ -50,7 +56,9 @@ import { TSecretDALFactory } from "./secret-dal"; import { interpolateSecrets } from "./secret-fns"; import { TCreateSecretReminderDTO, + TFailedIntegrationSyncEmailsPayload, THandleReminderDTO, + TIntegrationSyncPayload, TRemoveSecretReminderDTO, TSyncSecretsDTO } from "./secret-types"; @@ -509,6 +517,19 @@ export const secretQueueFactory = ({ ); }; + const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => { + const appCfg = getConfig(); + if (!appCfg.isSmtpConfigured) return; + + await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, { + jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`, + delay: 1_000 * 60, // 1 minute + + removeOnFail: true, + removeOnComplete: true + }); + }; + queueService.start(QueueName.SecretSync, async (job) => { const { _deDupeQueue: deDupeQueue, @@ -554,327 +575,396 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.IntegrationSync, async (job) => { - const { environment, actorId, isManual, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data; - if (depth > MAX_SYNC_SECRET_DEPTH) return; + if (job.name === QueueJobs.SendFailedIntegrationSyncEmails) { + const appCfg = getConfig(); - const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) { - throw new Error("Secret path not found"); - } + const jobPayload = job.data as TFailedIntegrationSyncEmailsPayload; - // find all imports made with the given environment and secret path - const linkSourceDto = { - projectId, - importEnv: folder.environment.id, - importPath: secretPath, - isReplication: false - }; - const imports = await secretImportDAL.find(linkSourceDto); + const projectMembers = await projectMembershipDAL.findAllProjectMembers(jobPayload.projectId); + const project = await projectDAL.findById(jobPayload.projectId); - if (imports.length) { - // keep calling sync secret for all the imports made - const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); - const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); - const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string); - logger.info( - `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` - ); - await Promise.all( - imports - .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string)) - // filter out already synced ones - .filter( - ({ folderId }) => - !deDupeQueue[ - uniqueSecretQueueKey( - foldersGroupedById[folderId][0]?.environmentSlug as string, - foldersGroupedById[folderId][0]?.path as string - ) - ] - ) - .map(({ folderId }) => - syncSecrets({ - projectId, - secretPath: foldersGroupedById[folderId][0]?.path as string, - environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, - _deDupeQueue: deDupeQueue, - _depth: depth + 1, - excludeReplication: true - }) - ) - ); - } - const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - let referencedFolderIds; - if (shouldUseSecretV2Bridge) { - const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences( - projectId, - folder.environment.slug, - secretPath - ); - referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); - } else { - const secretReferences = await secretDAL.findReferencedSecretReferences( - projectId, - folder.environment.slug, - secretPath - ); - referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); - } - if (referencedFolderIds.length) { - const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds); - const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string); - logger.info( - `getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` - ); - await Promise.all( - referencedFolderIds - .filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path)) - // filter out already synced ones - .filter( - (folderId) => - !deDupeQueue[ - uniqueSecretQueueKey( - referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, - referencedFoldersGroupedById[folderId][0]?.path as string - ) - ] - ) - .map((folderId) => - syncSecrets({ - projectId, - secretPath: referencedFoldersGroupedById[folderId][0]?.path as string, - environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, - _deDupeQueue: deDupeQueue, - _depth: depth + 1, - excludeReplication: true - }) - ) - ); - } - - const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment - const toBeSyncedIntegrations = integrations.filter( - // note: sync only the integrations sourced from secretPath - ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) - ); - - if (!integrations.length) return; - logger.info( - `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` - ); - - const lock = await keyStore.acquireLock( - [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)], - 10000, - { - retryCount: 3, - retryDelay: 2000 - } - ); - const lockAcquiredTime = new Date(); - - const lastRunSyncIntegrationTimestamp = await keyStore.getItem( - KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) - ); - - // check whether the integration should wait or not - if (lastRunSyncIntegrationTimestamp) { - const INTEGRATION_INTERVAL = 2000; - const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); - if (isStaleSyncIntegration) { - logger.info( - `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` + // Only send emails to admins, and if its a manual trigger, only send it to the person who triggered it (if actor is admin as well) + const filteredProjectMembers = projectMembers + .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) + .filter((member) => + jobPayload.manuallyTriggeredByUserId ? member.userId === jobPayload.manuallyTriggeredByUserId : true ); - return; - } - const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds( - lockAcquiredTime.toISOString(), - lastRunSyncIntegrationTimestamp - ); - if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0) - await new Promise((resolve) => { - setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000); - }); + await smtpService.sendMail({ + recipients: filteredProjectMembers.map((member) => member.user.email!), + template: SmtpTemplates.IntegrationSyncFailed, + subjectLine: `Integration Sync Failed`, + substitutions: { + syncMessage: jobPayload.count === 1 ? jobPayload.syncMessage : undefined, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array. + secretPath: jobPayload.secretPath, + environment: jobPayload.environmentName, + count: jobPayload.count, + projectName: project.name, + integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}` + } + }); } - const generateActor = async (): Promise => { - if (isManual && actorId) { - const user = await userDAL.findById(actorId); + if (job.name === QueueJobs.IntegrationSync) { + const { + environment, + actorId, + isManual, + projectId, + secretPath, + depth = 1, + deDupeQueue = {} + } = job.data as TIntegrationSyncPayload; + if (depth > MAX_SYNC_SECRET_DEPTH) return; - if (!user) { - throw new Error("User not found"); + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) { + throw new Error("Secret path not found"); + } + + // find all imports made with the given environment and secret path + const linkSourceDto = { + projectId, + importEnv: folder.environment.id, + importPath: secretPath, + isReplication: false + }; + const imports = await secretImportDAL.find(linkSourceDto); + + if (imports.length) { + // keep calling sync secret for all the imports made + const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); + const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); + const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string); + logger.info( + `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + await Promise.all( + imports + .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string)) + // filter out already synced ones + .filter( + ({ folderId }) => + !deDupeQueue[ + uniqueSecretQueueKey( + foldersGroupedById[folderId][0]?.environmentSlug as string, + foldersGroupedById[folderId][0]?.path as string + ) + ] + ) + .map(({ folderId }) => + syncSecrets({ + projectId, + secretPath: foldersGroupedById[folderId][0]?.path as string, + environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, + _deDupeQueue: deDupeQueue, + _depth: depth + 1, + excludeReplication: true + }) + ) + ); + } + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + let referencedFolderIds; + if (shouldUseSecretV2Bridge) { + const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences( + projectId, + folder.environment.slug, + secretPath + ); + referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); + } else { + const secretReferences = await secretDAL.findReferencedSecretReferences( + projectId, + folder.environment.slug, + secretPath + ); + referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); + } + if (referencedFolderIds.length) { + const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds); + const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string); + logger.info( + `getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + await Promise.all( + referencedFolderIds + .filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path)) + // filter out already synced ones + .filter( + (folderId) => + !deDupeQueue[ + uniqueSecretQueueKey( + referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, + referencedFoldersGroupedById[folderId][0]?.path as string + ) + ] + ) + .map((folderId) => + syncSecrets({ + projectId, + secretPath: referencedFoldersGroupedById[folderId][0]?.path as string, + environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, + _deDupeQueue: deDupeQueue, + _depth: depth + 1, + excludeReplication: true + }) + ) + ); + } + + const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment + const toBeSyncedIntegrations = integrations.filter( + // note: sync only the integrations sourced from secretPath + ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) + ); + + const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = []; + + if (!integrations.length) return; + logger.info( + `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + + const lock = await keyStore.acquireLock( + [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)], + 10000, + { + retryCount: 3, + retryDelay: 2000 + } + ); + const lockAcquiredTime = new Date(); + + const lastRunSyncIntegrationTimestamp = await keyStore.getItem( + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) + ); + + // check whether the integration should wait or not + if (lastRunSyncIntegrationTimestamp) { + const INTEGRATION_INTERVAL = 2000; + const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); + if (isStaleSyncIntegration) { + logger.info( + `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` + ); + return; + } + + const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds( + lockAcquiredTime.toISOString(), + lastRunSyncIntegrationTimestamp + ); + if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0) + await new Promise((resolve) => { + setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000); + }); + } + + const generateActor = async (): Promise => { + if (isManual && actorId) { + const user = await userDAL.findById(actorId); + + if (!user) { + throw new Error("User not found"); + } + + return { + type: ActorType.USER, + metadata: { + email: user.email, + username: user.username, + userId: user.id + } + }; } return { - type: ActorType.USER, - metadata: { - email: user.email, - username: user.username, - userId: user.id - } + type: ActorType.PLATFORM, + metadata: {} }; - } - - return { - type: ActorType.PLATFORM, - metadata: {} }; - }; - // akhilmhdh: this try catch is for lock release - try { - const secrets = shouldUseSecretV2Bridge - ? await getIntegrationSecretsV2({ - environment, - projectId, - folderId: folder.id, - depth: 1, - secretPath, - decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") - }) - : await getIntegrationSecrets({ - environment, - projectId, - folderId: folder.id, - key: botKey as string, - depth: 1, - secretPath - }); + // akhilmhdh: this try catch is for lock release + try { + const secrets = shouldUseSecretV2Bridge + ? await getIntegrationSecretsV2({ + environment, + projectId, + folderId: folder.id, + depth: 1, + secretPath, + decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") + }) + : await getIntegrationSecrets({ + environment, + projectId, + folderId: folder.id, + key: botKey as string, + depth: 1, + secretPath + }); - for (const integration of toBeSyncedIntegrations) { - const integrationAuth = { - ...integration.integrationAuth, - createdAt: new Date(), - updatedAt: new Date(), - projectId: integration.projectId - }; + for (const integration of toBeSyncedIntegrations) { + const integrationAuth = { + ...integration.integrationAuth, + createdAt: new Date(), + updatedAt: new Date(), + projectId: integration.projectId + }; - const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( - integrationAuth, - shouldUseSecretV2Bridge, - botKey - ); - let awsAssumeRoleArn = null; - if (shouldUseSecretV2Bridge) { - if (integrationAuth.encryptedAwsAssumeIamRoleArn) { - awsAssumeRoleArn = secretManagerDecryptor({ - cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) - }).toString(); - } - } else if ( - integrationAuth.awsAssumeIamRoleArnTag && - integrationAuth.awsAssumeIamRoleArnIV && - integrationAuth.awsAssumeIamRoleArnCipherText - ) { - awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, - iv: integrationAuth.awsAssumeIamRoleArnIV, - tag: integrationAuth.awsAssumeIamRoleArnTag, - key: botKey as string - }); - } - - const suffixedSecrets: typeof secrets = {}; - const metadata = integration.metadata as Record; - if (metadata) { - Object.keys(secrets).forEach((key) => { - const prefix = metadata?.secretPrefix || ""; - const suffix = metadata?.secretSuffix || ""; - const newKey = prefix + key + suffix; - suffixedSecrets[newKey] = secrets[key]; - }); - } - - // akhilmhdh: this try catch is for catching integration error and saving it in db - try { - // akhilmhdh: this needs to changed later to be more easier to use - // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places - const response = await syncIntegrationSecrets({ - createManySecretsRawFn, - updateManySecretsRawFn, - integrationDAL, - integration, + const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( integrationAuth, - secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, - accessId: accessId as string, - awsAssumeRoleArn, - accessToken, - projectId, - appendices: { - prefix: metadata?.secretPrefix || "", - suffix: metadata?.secretSuffix || "" - } - }); - - await auditLogService.createAuditLog({ - projectId, - actor: await generateActor(), - event: { - type: EventType.INTEGRATION_SYNCED, - metadata: { - integrationId: integration.id, - isSynced: response?.isSynced ?? true, - lastSyncJobId: job?.id ?? "", - lastUsed: new Date(), - syncMessage: response?.syncMessage ?? "" - } - } - }); - - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - lastUsed: new Date(), - syncMessage: response?.syncMessage ?? "", - isSynced: response?.isSynced ?? true - }); - } catch (err) { - logger.error( - err, - `Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]` + shouldUseSecretV2Bridge, + botKey ); - - const message = - (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || - "Unknown error occurred."; - - await auditLogService.createAuditLog({ - projectId, - actor: await generateActor(), - event: { - type: EventType.INTEGRATION_SYNCED, - metadata: { - integrationId: integration.id, - isSynced: false, - lastSyncJobId: job?.id ?? "", - lastUsed: new Date(), - syncMessage: message - } + let awsAssumeRoleArn = null; + if (shouldUseSecretV2Bridge) { + if (integrationAuth.encryptedAwsAssumeIamRoleArn) { + awsAssumeRoleArn = secretManagerDecryptor({ + cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) + }).toString(); } - }); + } else if ( + integrationAuth.awsAssumeIamRoleArnTag && + integrationAuth.awsAssumeIamRoleArnIV && + integrationAuth.awsAssumeIamRoleArnCipherText + ) { + awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, + iv: integrationAuth.awsAssumeIamRoleArnIV, + tag: integrationAuth.awsAssumeIamRoleArnTag, + key: botKey as string + }); + } - await integrationDAL.updateById(integration.id, { - lastSyncJobId: job.id, - syncMessage: message, - isSynced: false + const suffixedSecrets: typeof secrets = {}; + const metadata = integration.metadata as Record; + if (metadata) { + Object.keys(secrets).forEach((key) => { + const prefix = metadata?.secretPrefix || ""; + const suffix = metadata?.secretSuffix || ""; + const newKey = prefix + key + suffix; + suffixedSecrets[newKey] = secrets[key]; + }); + } + + // akhilmhdh: this try catch is for catching integration error and saving it in db + try { + // akhilmhdh: this needs to changed later to be more easier to use + // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places + const response = await syncIntegrationSecrets({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, + integration, + integrationAuth, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, + accessId: accessId as string, + awsAssumeRoleArn, + accessToken, + projectId, + appendices: { + prefix: metadata?.secretPrefix || "", + suffix: metadata?.secretSuffix || "" + } + }); + + await auditLogService.createAuditLog({ + projectId, + actor: await generateActor(), + event: { + type: EventType.INTEGRATION_SYNCED, + metadata: { + integrationId: integration.id, + isSynced: response?.isSynced ?? true, + lastSyncJobId: job?.id ?? "", + lastUsed: new Date(), + syncMessage: response?.syncMessage ?? "" + } + } + }); + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: response?.syncMessage ?? "", + isSynced: response?.isSynced ?? true + }); + + // May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check. + if (response?.isSynced === false) { + integrationsFailedToSync.push({ + integrationId: integration.id, + syncMessage: response.syncMessage + }); + } + } catch (err) { + logger.error( + err, + `Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]` + ); + + const message = + (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || + "Unknown error occurred."; + + await auditLogService.createAuditLog({ + projectId, + actor: await generateActor(), + event: { + type: EventType.INTEGRATION_SYNCED, + metadata: { + integrationId: integration.id, + isSynced: false, + lastSyncJobId: job?.id ?? "", + lastUsed: new Date(), + syncMessage: message + } + } + }); + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + syncMessage: message, + isSynced: false + }); + + integrationsFailedToSync.push({ + integrationId: integration.id, + syncMessage: message + }); + } + } + } finally { + await lock.release(); + if (integrationsFailedToSync.length) { + await sendFailedIntegrationSyncEmails({ + count: integrationsFailedToSync.length, + environmentName: folder.environment.name, + environmentSlug: environment, + ...(isManual && + actorId && { + manuallyTriggeredByUserId: actorId + }), + projectId, + secretPath, + syncMessage: integrationsFailedToSync[0].syncMessage }); } } - } finally { - await lock.release(); - } - await keyStore.setItemWithExpiry( - KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), - KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, - lockAcquiredTime.toISOString() - ); - logger.info("Secret integration sync ended: %s", job.id); + await keyStore.setItemWithExpiry( + KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), + KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, + lockAcquiredTime.toISOString() + ); + logger.info("Secret integration sync ended: %s", job.id); + } }); queueService.start(QueueName.SecretReminder, async ({ data }) => { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index df5030b8d..0bf1f1171 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1101,7 +1101,11 @@ export const secretServiceFactory = ({ return { secrets, imports }; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const { secrets, imports } = await getSecrets({ actorId, @@ -1262,7 +1266,10 @@ export const secretServiceFactory = ({ }); if (!botKey) - throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" }); + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); if (expandSecretReferences) { @@ -1354,7 +1361,11 @@ export const secretServiceFactory = ({ return { secret, type: SecretProtectionType.Direct as const }; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); @@ -1492,7 +1503,11 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secret }; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); @@ -1614,7 +1629,11 @@ export const secretServiceFactory = ({ }); return { type: SecretProtectionType.Direct as const, secret }; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); if (policy) { const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ policy, @@ -1714,7 +1733,11 @@ export const secretServiceFactory = ({ return { secrets, type: SecretProtectionType.Direct as const }; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const sanitizedSecrets = inputSecrets.map( ({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => { const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey); @@ -1836,7 +1859,11 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secrets }; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); const sanitizedSecrets = inputSecrets.map( ({ secretComment, @@ -1964,7 +1991,11 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secrets }; } - if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + if (!botKey) + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); if (policy) { const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ @@ -2298,7 +2329,10 @@ export const secretServiceFactory = ({ } if (!botKey) - throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" }); + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); await secretDAL.transaction(async (tx) => { const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx); @@ -2381,7 +2415,10 @@ export const secretServiceFactory = ({ const { botKey } = await projectBotService.getBotKey(project.id); if (!botKey) { - throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + throw new BadRequestError({ + message: "Project bot not found. Please upgrade your project.", + name: "bot_not_found_error" + }); } const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath); diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index fd2dff329..7c09c9349 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; +import { z } from "zod"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; @@ -21,6 +22,29 @@ type TPartialSecret = Pick; +export const FailedIntegrationSyncEmailsPayloadSchema = z.object({ + projectId: z.string(), + secretPath: z.string(), + environmentName: z.string(), + environmentSlug: z.string(), + + count: z.number(), + syncMessage: z.string().optional(), + manuallyTriggeredByUserId: z.string().optional() +}); + +export type TFailedIntegrationSyncEmailsPayload = z.infer; + +export type TIntegrationSyncPayload = { + isManual?: boolean; + actorId?: string; + projectId: string; + environment: string; + secretPath: string; + depth?: number; + deDupeQueue?: Record; +}; + export type TCreateSecretDTO = { secretName: string; path: string; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index d0e1e0774..823da4cca 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -33,7 +33,8 @@ export enum SmtpTemplates { SecretLeakIncident = "secretLeakIncident.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars", ScimUserProvisioned = "scimUserProvisioned.handlebars", - PkiExpirationAlert = "pkiExpirationAlert.handlebars" + PkiExpirationAlert = "pkiExpirationAlert.handlebars", + IntegrationSyncFailed = "integrationSyncFailed.handlebars" } export enum SmtpHost { diff --git a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars new file mode 100644 index 000000000..5c5d76693 --- /dev/null +++ b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars @@ -0,0 +1,31 @@ + + + + + + Integration Sync Failed + + + +

Infisical

+ +
+

{{count}} integration(s) failed to sync.

+ + View your project integrations. + +
+ +
+
+

Project: {{projectName}}

+

Environment: {{environment}}

+

Secret Path: {{secretPath}}

+
+ + {{#if syncMessage}} +

Reason: {{syncMessage}}

+ {{/if}} + + + \ No newline at end of file diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 482c6f78a..e40c07022 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc. package cmd import ( + "fmt" "os" "strings" @@ -43,14 +44,26 @@ func init() { rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.") rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { silent, err := cmd.Flags().GetBool("silent") - config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL) if err != nil { util.HandleError(err) } + config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL) + if !util.IsRunningInDocker() && !silent { util.CheckForUpdate() } + + loggedInDetails, err := util.GetCurrentLoggedInUserDetails() + + if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired { + token, err := util.GetInfisicalToken(cmd) + + if err == nil && token != nil { + util.PrintWarning(fmt.Sprintf("Your logged-in session is being overwritten by the token provided from the %s.", token.Source)) + } + } + } // if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index c9ac36852..e2987cc7d 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -160,19 +160,19 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - if (token == nil) { + if token == nil { util.RequireLocalWorkspaceFile() } environmentName, _ := cmd.Flags().GetString("env") if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() + environmentFromWorkspace := util.GetEnvFromWorkspaceFile() if environmentFromWorkspace != "" { environmentName = environmentFromWorkspace } } - projectId, err := cmd.Flags().GetString("projectId") + projectId, err := cmd.Flags().GetString("projectId") if err != nil { util.HandleError(err, "Unable to parse flag") } diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 1bad5e327..62ff07190 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -63,8 +63,9 @@ type DynamicSecretLease struct { } type TokenDetails struct { - Type string - Token string + Type string + Token string + Source string } type SingleFolder struct { diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index b758ebd9d..11a1e3e0a 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -87,11 +87,15 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro return nil, err } + var source = "--token flag" + if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable. infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME) + source = fmt.Sprintf("%s environment variable", INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME) if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable. infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) + source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME) } } @@ -101,14 +105,16 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro if strings.HasPrefix(infisicalToken, "st.") { return &models.TokenDetails{ - Type: SERVICE_TOKEN_IDENTIFIER, - Token: infisicalToken, + Type: SERVICE_TOKEN_IDENTIFIER, + Token: infisicalToken, + Source: source, }, nil } return &models.TokenDetails{ - Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER, - Token: infisicalToken, + Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER, + Token: infisicalToken, + Source: source, }, nil } diff --git a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx new file mode 100644 index 000000000..8a71772b1 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx @@ -0,0 +1,164 @@ +--- +title: "Azure Entra Id" +description: "Learn how to dynamically generate Azure Entra Id user credentials." +--- + +The Infisical Azure Entra Id dynamic secret allows you to generate Azure Entra Id credentials on demand based on configured role. + +## Prerequisites + + + +Login to [Microsoft Entra ID](https://entra.microsoft.com/) + + + +Go to Overview, Copy and store `Tenant Id` +![Copy Tenant Id](../../../images/platform/dynamic-secrets/dynamic-secret-ad-tenant-id.png) + + + +Go to Applications > App registrations. Click on New Registration. +![Copy Tenant Id](../../../images/platform/dynamic-secrets/dynamic-secret-ad-new-registration.png) + + + +Enter an application name. Click Register. + + + +Copy and store `Application Id`. +![Copy Application Id](../../../images/platform/dynamic-secrets/dynamic-secret-ad-copy-app-id.png) + + + +Go to Clients and Secrets. Click on New Client Secret. + + + +Enter a description, select expiry and click Add. + + + +Copy and store `Client Secret` value. +![Copy client Secret](../../../images/platform/dynamic-secrets/dynamic-secret-ad-add-client-secret.png) + + + +Go to API Permissions. Click on Add a permission. +![Click add a permission](../../../images/platform/dynamic-secrets/dynamic-secret-ad-add-permission.png) + + + +Click on Microsoft Graph. +![Click Microsoft Graph](../../../images/platform/dynamic-secrets/dynamic-secret-ad-select-graph.png) + + + +Click on Application Permissions. Search and select `User.ReadWrite.All` and click Add permissions. +![Add User.Read.All](../../../images/platform/dynamic-secrets/dynamic-secret-ad-select-perms.png) + + + +Click on Grant admin consent for app. Click yes to confirm. +![Grant admin consent](../../../images/platform/dynamic-secrets/dynamic-secret-ad-admin-consent.png) + + + +Go to Dashboard. Click on show more. +![Show more](../../../images/platform/dynamic-secrets/dynamic-secret-ad-show-more.png) + + + +Click on Roles & admins. Search for User Administrator and click on it. +![User Administrator](../../../images/platform/dynamic-secrets/dynamic-secret-ad-user-admin.png) + + + +Click on Add assignments. Search for the application name you created and select it. Click on Add. +![Add assignments](../../../images/platform/dynamic-secrets/dynamic-secret-ad-add-assignments.png) + + + +## Set up Dynamic Secrets with Azure Entra ID + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ad-modal.png) + + + + Prefix for the secrets to be created + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret. + + + + The Tenant ID of your Azure Entra ID account. + + + + The Application ID of the application you created in Azure Entra ID. + + + + The Client Secret of the application you created in Azure Entra ID. + + + + Multi select list of users to generate secrets for. + + + + + After submitting the form, you will see a dynamic secrets for each user created in the dashboard. + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ad-lease.png) + + + +## Audit or Revoke Leases +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the expiration time of the lease or delete a lease before it's set time to live. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) + +## Renew Leases +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + diff --git a/docs/documentation/platform/workflow-integrations/slack-integration.mdx b/docs/documentation/platform/workflow-integrations/slack-integration.mdx index 18172a2a5..92b3feeca 100644 --- a/docs/documentation/platform/workflow-integrations/slack-integration.mdx +++ b/docs/documentation/platform/workflow-integrations/slack-integration.mdx @@ -5,9 +5,11 @@ description: "Learn how to setup Slack integration" This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects. +## Setting up Slack integration in your projects + - ## Create Slack workflow integration + ### Create Slack workflow integration In order to use Slack integration in your projects, you will first have to @@ -32,7 +34,7 @@ This guide will provide step by step instructions on how to configure Slack inte - ## Configure project to use Slack workflow integration + ### Configure project to use Slack workflow integration @@ -56,7 +58,7 @@ This guide will provide step by step instructions on how to configure Slack inte - ## Configure admin settings + ### Configure admin settings Note that this step only has to be done once for the entire instance. @@ -90,7 +92,7 @@ This guide will provide step by step instructions on how to configure Slack inte - ## Create Slack workflow integration + ### Create Slack workflow integration @@ -116,7 +118,7 @@ This guide will provide step by step instructions on how to configure Slack inte - ## Configure project to use Slack workflow integration + ### Configure project to use Slack workflow integration @@ -140,3 +142,23 @@ This guide will provide step by step instructions on how to configure Slack inte + +## Using the Slack integration in your private channels + + + + ![private slack setup + menu](/images/platform/workflow-integrations/slack-integration/private-slack-setup-menu.png) + + + ![private slack setup + add](/images/platform/workflow-integrations/slack-integration/private-slack-setup-add.png) + + + ![private slack setup + form](/images/platform/workflow-integrations/slack-integration/private-slack-setup-form.png) + You can now view the private channels in the Slack channel selection fields! + ![private slack setup + channels](/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png) + + diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-assignments.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-assignments.png new file mode 100644 index 000000000..561639de0 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-assignments.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-client-secret.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-client-secret.png new file mode 100644 index 000000000..358fc53c1 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-client-secret.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-permission.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-permission.png new file mode 100644 index 000000000..20614f9dc Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-add-permission.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-admin-consent.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-admin-consent.png new file mode 100644 index 000000000..5c8102450 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-admin-consent.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-copy-app-id.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-copy-app-id.png new file mode 100644 index 000000000..aa36ee39d Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-copy-app-id.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-lease.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-lease.png new file mode 100644 index 000000000..4740062db Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-lease.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-modal.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-modal.png new file mode 100644 index 000000000..481c78923 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-new-registration.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-new-registration.png new file mode 100644 index 000000000..285df9d77 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-new-registration.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-select-graph.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-select-graph.png new file mode 100644 index 000000000..98f23c084 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-select-graph.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-select-perms.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-select-perms.png new file mode 100644 index 000000000..45abaa738 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-select-perms.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-show-more.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-show-more.png new file mode 100644 index 000000000..df4dc9567 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-show-more.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-tenant-id.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-tenant-id.png new file mode 100644 index 000000000..5b0cb4763 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-tenant-id.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-ad-user-admin.png b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-user-admin.png new file mode 100644 index 000000000..dfc1deadd Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-ad-user-admin.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-add.png b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-add.png new file mode 100644 index 000000000..1945313e8 Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-add.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png new file mode 100644 index 000000000..3f6bd0d1d Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-form.png b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-form.png new file mode 100644 index 000000000..7d5ac0560 Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-form.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-menu.png b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-menu.png new file mode 100644 index 000000000..087f31483 Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-menu.png differ diff --git a/docs/mint.json b/docs/mint.json index 60f7bbbcb..2fbce096d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -167,7 +167,8 @@ "documentation/platform/dynamic-secrets/rabbit-mq", "documentation/platform/dynamic-secrets/aws-iam", "documentation/platform/dynamic-secrets/mongo-atlas", - "documentation/platform/dynamic-secrets/mongo-db" + "documentation/platform/dynamic-secrets/mongo-db", + "documentation/platform/dynamic-secrets/azure-entra-id" ] }, { diff --git a/docs/sdks/languages/node.mdx b/docs/sdks/languages/node.mdx index 9a25dd43e..04f210db0 100644 --- a/docs/sdks/languages/node.mdx +++ b/docs/sdks/languages/node.mdx @@ -1,9 +1,11 @@ --- title: "Infisical Node.js SDK" sidebarTitle: "Node.js" +url: "https://github.com/Infisical/node-sdk-v2" icon: "node" --- +{/* If you're working with Node.js, the official [Infisical Node SDK](https://github.com/Infisical/sdk/tree/main/languages/node) package is the easiest way to fetch and work with secrets for your application. - [NPM Package](https://www.npmjs.com/package/@infisical/sdk) @@ -552,3 +554,5 @@ const decryptedString = await client.decryptSymmetric({ #### Returns (string) `plaintext` (string): The decrypted plaintext. + +*/} \ No newline at end of file diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index e502fdd4e..11d34bb38 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -10,7 +10,7 @@ From local development to production, Infisical SDKs provide the easiest way for - Fetch secrets on demand - + Manage secrets for your Node application on demand diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 95104cdf1..5227088b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "frontend", + "name": "relock-npm-lock-v2-SvMQeF", "lockfileVersion": 3, "requires": true, "packages": { @@ -65,7 +65,7 @@ "i18next-http-backend": "^2.2.0", "infisical-node": "^1.0.37", "ip": "^2.0.1", - "jspdf": "^2.5.1", + "jspdf": "^2.5.2", "jsrp": "^0.2.4", "jwt-decode": "^3.1.2", "lottie-react": "^2.4.0", @@ -12729,9 +12729,10 @@ } }, "node_modules/dompurify": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", - "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==", + "license": "(MPL-2.0 OR Apache-2.0)", "optional": true }, "node_modules/domutils": { @@ -16873,22 +16874,29 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/jspdf": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", - "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.14.0", + "@babel/runtime": "^7.23.2", "atob": "^2.1.2", "btoa": "^1.2.1", - "fflate": "^0.4.8" + "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.6", "core-js": "^3.6.0", - "dompurify": "^2.2.0", + "dompurify": "^2.5.4", "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jspdf/node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index a538e7cf1..42467ced4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -73,7 +73,7 @@ "i18next-http-backend": "^2.2.0", "infisical-node": "^1.0.37", "ip": "^2.0.1", - "jspdf": "^2.5.1", + "jspdf": "^2.5.2", "jsrp": "^0.2.4", "jwt-decode": "^3.1.2", "lottie-react": "^2.4.0", diff --git a/frontend/src/components/features/FormLabelToolTip.tsx b/frontend/src/components/features/FormLabelToolTip.tsx new file mode 100644 index 000000000..584c47aae --- /dev/null +++ b/frontend/src/components/features/FormLabelToolTip.tsx @@ -0,0 +1,36 @@ +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { FormLabel, Tooltip } from "../v2"; + +// To give users example of possible values of TTL +export const FormLabelToolTip = ({ label, linkToMore, content }: { label: string, linkToMore: string, content: string }) => ( +
+ + {content}{" "} + + More + + + } + > + + + } + /> +
+); diff --git a/frontend/src/components/features/TtlFormLabel.tsx b/frontend/src/components/features/TtlFormLabel.tsx index 14382abb4..5278feec7 100644 --- a/frontend/src/components/features/TtlFormLabel.tsx +++ b/frontend/src/components/features/TtlFormLabel.tsx @@ -1,36 +1,12 @@ -import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { FormLabel, Tooltip } from "../v2"; +import { FormLabelToolTip } from "./FormLabelToolTip"; // To give users example of possible values of TTL export const TtlFormLabel = ({ label }: { label: string }) => (
- - 1m, 2h, 3d.{" "} - - More - - - } - > - - - } + content="1m, 2h, 3d. " + linkToMore="https://github.com/vercel/ms?tab=readme-ov-file#examples" />
); diff --git a/frontend/src/hooks/api/dynamicSecret/queries.ts b/frontend/src/hooks/api/dynamicSecret/queries.ts index 481b431cc..f84fd0712 100644 --- a/frontend/src/hooks/api/dynamicSecret/queries.ts +++ b/frontend/src/hooks/api/dynamicSecret/queries.ts @@ -71,6 +71,34 @@ export const useGetDynamicSecretDetails = ({ }); }; +export const useGetDynamicSecretProviderData = ({ + tenantId, + applicationId, + clientSecret, + enabled +}: { + tenantId: string; + applicationId: string; + clientSecret: string; + enabled: boolean +}) => { + return useQuery({ + queryKey: ["users"], + queryFn: async () => { + const { data } = await apiRequest.post<{id:string, email: string, name:string}[]>( + "/api/v1/dynamic-secrets/entra-id/users", + { + tenantId, + applicationId, + clientSecret + } + ); + return data; + }, + enabled + }); +}; + export const useGetDynamicSecretsOfAllEnv = ({ path, projectSlug, diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 32c9b15d0..e97234bc4 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -24,7 +24,8 @@ export enum DynamicSecretProviders { MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", MongoDB = "mongo-db", - RabbitMq = "rabbit-mq" + RabbitMq = "rabbit-mq", + AzureEntraId = "azure-entra-id" } export enum SqlProviders { @@ -177,7 +178,17 @@ export type TDynamicSecretProvider = }; ca?: string; }; - }; + } + | { + type: DynamicSecretProviders.AzureEntraId; + inputs: { + tenantId: string; + userId: string; + email: string; + applicationId: string; + clientSecret: string; + }; + }; export type TCreateDynamicSecretDTO = { projectSlug: string; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx new file mode 100644 index 000000000..2fb2250a0 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -0,0 +1,355 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faArrowUpRightFromSquare, faBookOpen, faCheckCircle, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input +} from "@app/components/v2"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/v2/Dropdown/Dropdown"; +import { Tooltip } from "@app/components/v2/Tooltip"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + selectedUsers: z.array(z.object({ + id: z.string().min(1), + name: z.string().min(1), + email: z.string().min(1), + })), + provider: z.object({ + tenantId: z.string().min(1), + applicationId: z.string().min(1), + clientSecret: z.string().min(1) + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().min(1).refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const AzureEntraIdInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + watch, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema) + }); + const tenantId = watch("provider.tenantId"); + const applicationId = watch("provider.applicationId"); + const clientSecret = watch("provider.clientSecret"); + + const configurationComplete = !!(tenantId && applicationId && clientSecret); + const { data, isLoading, isError, isFetching } = useGetDynamicSecretProviderData({ tenantId, applicationId, clientSecret, enabled: !!configurationComplete }); + const loading = configurationComplete && isFetching; + const errored = configurationComplete && !isFetching && isError; + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, selectedUsers, provider, maxTTL, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + selectedUsers.map(async (user: { id: string, name: string, email: string }) => { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: user.id, tenantId: provider.tenantId, email: user.email, applicationId: provider.applicationId, clientSecret: provider.clientSecret } }, + maxTTL, + name: `${name}-${user.name}`, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration + + +
+ + Docs + +
+
+ +
+
+
+ ( + + + + )} + + /> +
+
+
+
+ ( + + + + )} + + /> +
+
+
+
+ ( + + + + )} + + /> +
+
+
+
+ +
+ Select Users +
+
+   We create a unique dynamic secret for each user in Entra Id. +
+
+
+ ( + + + +
+ } + > +
+ + +
+ + + + {data && data.map((user) => { + const ids = value?.map((selectedUser) => selectedUser.id) + const isChecked = ids?.includes(user.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el) => el.id !== user.id) + : [...(value || []), user] + ); + }} + key={`create-policy-members-${user.id}`} + iconPos="right" + icon={isChecked && } + > + {user.name}
{`(${user.email})`} +
+ ); + })} +
+ + + )} + /> +
+
+
+
+
+ + +
+ + + ); +}; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 90eb74012..a28544a33 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { DiRedis } from "react-icons/di"; -import { SiApachecassandra, SiElasticsearch, SiMongodb, SiRabbitmq } from "react-icons/si"; +import { SiApachecassandra, SiElasticsearch, SiMicrosoftazure, SiMongodb, SiRabbitmq } from "react-icons/si"; import { faAws } from "@fortawesome/free-brands-svg-icons"; import { faDatabase } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -11,6 +11,7 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm"; import { AwsIamInputForm } from "./AwsIamInputForm"; +import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm"; import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; @@ -77,6 +78,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.RabbitMq, title: "RabbitMQ" + }, + { + icon: , + provider: DynamicSecretProviders.AzureEntraId, + title: "Azure Entra ID", } ]; @@ -300,6 +306,25 @@ export const CreateDynamicSecretForm = ({ /> )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.AzureEntraId && ( + + + + ) + } diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 85be20fc1..c99fdda16 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -176,6 +176,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.AzureEntraId) { + const { email, password } = data as { + email: string; + password: string; + }; + + return ( +
+ + +
+ ); + } + return null; }; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx new file mode 100644 index 000000000..31ac7b5c2 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx @@ -0,0 +1,283 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + SecretInput, +} from "@app/components/v2"; +import { useUpdateDynamicSecret } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + inputs: z.object({ + email: z.string(), + userId: z.string(), + tenantId: z.string(), + applicationId: z.string(), + clientSecret: z.string() + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + newName: z + .string() + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .optional() +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + environment: string; + projectSlug: string; +}; + +export const EditDynamicSecretAzureEntraIdForm = ({ + onClose, + dynamicSecret, + secretPath, + environment, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + values: { + defaultTTL: dynamicSecret.defaultTTL, + maxTTL: dynamicSecret.maxTTL, + newName: dynamicSecret.name, + inputs: { + ...(dynamicSecret.inputs as TForm["inputs"]) + } + } + }); + + const updateDynamicSecret = useUpdateDynamicSecret(); + + const handleUpdateDynamicSecret = async ({ maxTTL, defaultTTL, newName, inputs }: TForm) => { + // wait till previous request is finished + if (updateDynamicSecret.isLoading) return; + try { + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + newName: newName === dynamicSecret.name ? undefined : newName, + inputs + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to update dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+
+ ( + + + + )} + /> +
+
+
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx index 8f95bcc94..23cb45843 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx @@ -6,6 +6,7 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { EditDynamicSecretAwsElastiCacheProviderForm } from "./EditDynamicSecretAwsElastiCacheProviderForm"; import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm"; +import { EditDynamicSecretAzureEntraIdForm } from "./EditDynamicSecretAzureEntraIdForm"; import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm"; import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; @@ -202,6 +203,24 @@ export const EditDynamicSecretForm = ({ /> )} + + {dynamicSecretDetails?.type === DynamicSecretProviders.AzureEntraId && ( + + + + )} ); }; diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx index 2aa552f52..b37c22012 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx @@ -66,12 +66,11 @@ export const ManagePlansTable = ({ billingCycle }: Props) => { {subscription && !isTableDataLoading && tableData && - tableData.rows.map(({ name, starter, team, pro, enterprise }) => { + tableData.rows.map(({ name, starter, pro, enterprise }) => { return ( {displayCell(name)} {displayCell(starter)} - {displayCell(team)} {displayCell(pro)} {displayCell(enterprise)} diff --git a/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx b/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx index b07dde77e..9bb005ce4 100644 --- a/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/IntegrationPanel.tsx @@ -5,6 +5,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api"; const slackFormSchema = z.object({ @@ -65,6 +66,8 @@ export const IntegrationPanel = () => { const { data: adminSlackConfig } = useGetAdminSlackConfig(); const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); + const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle(); + const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle(); useEffect(() => { if (adminSlackConfig) { @@ -120,6 +123,9 @@ export const IntegrationPanel = () => { setIsSlackClientIdFocused.on()} + onBlur={() => setIsSlackClientIdFocused.off()} onChange={(e) => field.onChange(e.target.value)} /> @@ -138,6 +144,9 @@ export const IntegrationPanel = () => { setIsSlackClientSecretFocused.on()} + onBlur={() => setIsSlackClientSecretFocused.off()} onChange={(e) => field.onChange(e.target.value)} />