chore: resolve merge conflict

This commit is contained in:
Scott Wilson
2024-09-20 12:20:13 -07:00
62 changed files with 2482 additions and 740 deletions
+317 -100
View File
@@ -81,7 +81,7 @@
"pino": "^8.16.2", "pino": "^8.16.2",
"pkijs": "^3.2.4", "pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.3.8",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"scim-patch": "^0.8.3", "scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10", "scim2-parse-filter": "^0.2.10",
@@ -8018,6 +8018,7 @@
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": { "dependencies": {
"mime-types": "~2.1.34", "mime-types": "~2.1.34",
"negotiator": "0.6.3" "negotiator": "0.6.3"
@@ -8336,7 +8337,8 @@
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "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": { "node_modules/array-includes": {
"version": "3.1.7", "version": "3.1.7",
@@ -8814,9 +8816,10 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@@ -8826,7 +8829,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@@ -8840,6 +8843,7 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -8848,6 +8852,7 @@
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
}, },
@@ -8858,7 +8863,8 @@
"node_modules/body-parser/node_modules/ms": { "node_modules/body-parser/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
}, },
"node_modules/bottleneck": { "node_modules/bottleneck": {
"version": "2.19.5", "version": "2.19.5",
@@ -9006,6 +9012,7 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -9028,13 +9035,19 @@
} }
}, },
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.5", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1", "get-intrinsic": "^1.2.4",
"set-function-length": "^1.1.1" "set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -9379,6 +9392,7 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -9543,16 +9557,20 @@
} }
}, },
"node_modules/define-data-property": { "node_modules/define-data-property": {
"version": "1.1.1", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.1", "es-define-property": "^1.0.0",
"gopd": "^1.0.1", "es-errors": "^1.3.0",
"has-property-descriptors": "^1.0.0" "gopd": "^1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/define-lazy-prop": { "node_modules/define-lazy-prop": {
@@ -9618,6 +9636,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8", "node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
@@ -9724,7 +9743,8 @@
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "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": { "node_modules/electron-to-chromium": {
"version": "1.4.816", "version": "1.4.816",
@@ -9738,9 +9758,10 @@
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -9827,6 +9848,27 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/es-set-tostringtag": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
@@ -10452,6 +10494,7 @@
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -10495,36 +10538,37 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@@ -10588,6 +10632,7 @@
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -10595,12 +10640,14 @@
"node_modules/express/node_modules/cookie-signature": { "node_modules/express/node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "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": { "node_modules/express/node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -10608,7 +10655,8 @@
"node_modules/express/node_modules/ms": { "node_modules/express/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
}, },
"node_modules/extend": { "node_modules/extend": {
"version": "3.0.2", "version": "3.0.2",
@@ -10815,12 +10863,13 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@@ -10835,6 +10884,7 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -10842,7 +10892,8 @@
"node_modules/finalhandler/node_modules/ms": { "node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "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": { "node_modules/find-my-way": {
"version": "8.1.0", "version": "8.1.0",
@@ -11008,6 +11059,7 @@
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -11365,15 +11417,20 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.2", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"has-proto": "^1.0.1", "has-proto": "^1.0.1",
"has-symbols": "^1.0.3", "has-symbols": "^1.0.3",
"hasown": "^2.0.0" "hasown": "^2.0.0"
}, },
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@@ -11719,11 +11776,12 @@
} }
}, },
"node_modules/has-property-descriptors": { "node_modules/has-property-descriptors": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.2" "es-define-property": "^1.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -13276,6 +13334,7 @@
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -13286,9 +13345,13 @@
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@@ -13309,6 +13372,7 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -13748,6 +13812,7 @@
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -14099,6 +14164,7 @@
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": { "dependencies": {
"ee-first": "1.1.1" "ee-first": "1.1.1"
}, },
@@ -14511,9 +14577,10 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@@ -14716,20 +14783,78 @@
} }
}, },
"node_modules/pino-http": { "node_modules/pino-http": {
"version": "8.6.1", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.6.1.tgz", "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.3.0.tgz",
"integrity": "sha512-J0hiJgUExtBXP2BjrK4VB305tHXS31sCmWJ9XJo2wPkLHa1NFPuW4V9wjG27PAc2fmBCigiNhQKpvrx+kntBPA==", "integrity": "sha512-kaHQqt1i5S9LXWmyuw6aPPqYW/TjoDPizPs4PnDW4hSpajz2Uo/oisNliLf7We1xzpiLacdntmw8yaZiEkppQQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"get-caller-file": "^2.0.5", "get-caller-file": "^2.0.5",
"pino": "^8.17.1", "pino": "^9.0.0",
"pino-std-serializers": "^6.2.2", "pino-std-serializers": "^7.0.0",
"process-warning": "^3.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": { "node_modules/pino-http/node_modules/process-warning": {
"version": "3.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz",
"integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" "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": { "node_modules/pino-pretty": {
"version": "10.2.3", "version": "10.2.3",
@@ -15096,9 +15221,10 @@
} }
}, },
"node_modules/probot": { "node_modules/probot": {
"version": "13.0.0", "version": "13.3.8",
"resolved": "https://registry.npmjs.org/probot/-/probot-13.0.0.tgz", "resolved": "https://registry.npmjs.org/probot/-/probot-13.3.8.tgz",
"integrity": "sha512-3ht9kAJ+ISjLyWLLCKVdrLE5xs/x+zUx07J5kYTxAyIxUvwF6Acr8xT5fiNihbBHAsEl4+A4CMYZQvZ5hx5bgw==", "integrity": "sha512-xc+KBC0mp1JKFMsPbMyj1SpmN0B7Q8uFO7ze4PBbNv74q8AyPGqYL3TmkZSOmcOjFTeFrZTnMYEoXi+z1anyLA==",
"license": "ISC",
"dependencies": { "dependencies": {
"@octokit/core": "^5.0.2", "@octokit/core": "^5.0.2",
"@octokit/plugin-enterprise-compatibility": "^4.0.1", "@octokit/plugin-enterprise-compatibility": "^4.0.1",
@@ -15113,19 +15239,18 @@
"@probot/octokit-plugin-config": "^2.0.1", "@probot/octokit-plugin-config": "^2.0.1",
"@probot/pino": "^2.3.5", "@probot/pino": "^2.3.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"commander": "^11.1.0", "bottleneck": "^2.19.5",
"commander": "^12.0.0",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eventsource": "^2.0.2", "express": "^4.21.0",
"express": "^4.18.2",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lru-cache": "^10.0.3", "lru-cache": "^10.0.3",
"octokit-auth-probot": "^2.0.0", "octokit-auth-probot": "^2.0.0",
"pino": "^8.16.1", "pino": "^9.0.0",
"pino-http": "^8.5.1", "pino-http": "^10.0.0",
"pkg-conf": "^3.1.0", "pkg-conf": "^3.1.0",
"resolve": "^1.22.8",
"update-dotenv": "^1.1.1" "update-dotenv": "^1.1.1"
}, },
"bin": { "bin": {
@@ -15152,11 +15277,12 @@
} }
}, },
"node_modules/probot/node_modules/commander": { "node_modules/probot/node_modules/commander": {
"version": "11.1.0", "version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=16" "node": ">=18"
} }
}, },
"node_modules/probot/node_modules/lru-cache": { "node_modules/probot/node_modules/lru-cache": {
@@ -15167,6 +15293,68 @@
"node": "14 || >=16.14" "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": { "node_modules/process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -15282,11 +15470,12 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -15359,6 +15548,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -15367,6 +15557,7 @@
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
@@ -15381,6 +15572,7 @@
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
}, },
@@ -15961,9 +16153,10 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@@ -15987,6 +16180,7 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -15994,12 +16188,23 @@
"node_modules/send/node_modules/debug/node_modules/ms": { "node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "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": { "node_modules/send/node_modules/mime": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": { "bin": {
"mime": "cli.js" "mime": "cli.js"
}, },
@@ -16013,14 +16218,15 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": { "dependencies": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@@ -16037,14 +16243,17 @@
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
}, },
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.1.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": { "dependencies": {
"define-data-property": "^1.1.1", "define-data-property": "^1.1.4",
"get-intrinsic": "^1.2.1", "es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1", "gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0" "has-property-descriptors": "^1.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -16103,13 +16312,18 @@
} }
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.0.4", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"license": "MIT",
"dependencies": { "dependencies": {
"call-bind": "^1.0.0", "call-bind": "^1.0.7",
"get-intrinsic": "^1.0.2", "es-errors": "^1.3.0",
"object-inspect": "^1.9.0" "get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -17704,6 +17918,7 @@
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": { "dependencies": {
"media-typer": "0.3.0", "media-typer": "0.3.0",
"mime-types": "~2.1.24" "mime-types": "~2.1.24"
@@ -17927,6 +18142,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -18051,6 +18267,7 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
+1 -1
View File
@@ -178,7 +178,7 @@
"pino": "^8.16.2", "pino": "^8.16.2",
"pkijs": "^3.2.4", "pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.3.8",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"scim-patch": "^0.8.3", "scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10", "scim2-parse-filter": "^0.2.10",
@@ -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({ server.route({
method: "PATCH", method: "PATCH",
url: "/:name", url: "/:name",
@@ -23,6 +23,7 @@ import {
TListDynamicSecretsMultiEnvDTO, TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO TUpdateDynamicSecretDTO
} from "./dynamic-secret-types"; } from "./dynamic-secret-types";
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
type TDynamicSecretServiceFactoryDep = { type TDynamicSecretServiceFactoryDep = {
@@ -462,6 +463,23 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg; 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 { return {
create, create,
updateByName, updateByName,
@@ -470,6 +488,7 @@ export const dynamicSecretServiceFactory = ({
listDynamicSecretsByEnv, listDynamicSecretsByEnv,
listDynamicSecretsByFolderIds, listDynamicSecretsByFolderIds,
getDynamicSecretCount, getDynamicSecretCount,
getCountMultiEnv getCountMultiEnv,
fetchAzureEntraIdUsers
}; };
}; };
@@ -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<User[]>;
} => {
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
};
};
@@ -1,5 +1,6 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam"; import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra"; import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search"; import { ElasticSearchProvider } from "./elastic-search";
import { DynamicSecretProviders } from "./models"; import { DynamicSecretProviders } from "./models";
@@ -18,5 +19,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(), [DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
[DynamicSecretProviders.MongoDB]: MongoDBProvider(), [DynamicSecretProviders.MongoDB]: MongoDBProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(), [DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider() [DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider()
}); });
@@ -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 { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
Cassandra = "cassandra", Cassandra = "cassandra",
@@ -175,7 +183,8 @@ export enum DynamicSecretProviders {
MongoAtlas = "mongo-db-atlas", MongoAtlas = "mongo-db-atlas",
ElasticSearch = "elastic-search", ElasticSearch = "elastic-search",
MongoDB = "mongo-db", MongoDB = "mongo-db",
RabbitMq = "rabbit-mq" RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ 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.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), 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 = { export type TDynamicProviderFns = {
@@ -1,7 +1,5 @@
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
import { conditionsMatcher } from "@app/lib/casl";
export enum OrgPermissionActions { export enum OrgPermissionActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -48,7 +46,7 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions // ws permissions
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
@@ -115,13 +113,13 @@ const buildAdminPermission = () => {
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return build({ conditionsMatcher }); return rules;
}; };
export const orgAdminPermissions = buildAdminPermission(); export const orgAdminPermissions = buildAdminPermission();
const buildMemberPermission = () => { const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
@@ -142,14 +140,14 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
return build({ conditionsMatcher }); return rules;
}; };
export const orgMemberPermissions = buildMemberPermission(); export const orgMemberPermissions = buildMemberPermission();
const buildNoAccessPermission = () => { const buildNoAccessPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher }); return rules;
}; };
export const orgNoAccessPermissions = buildNoAccessPermission(); export const orgNoAccessPermissions = buildNoAccessPermission();
@@ -1,7 +1,13 @@
import { z } from "zod"; import { z } from "zod";
import { TDbClient } from "@app/db"; 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 { DatabaseError } from "@app/lib/errors";
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@@ -10,18 +16,91 @@ export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
export const permissionDALFactory = (db: TDbClient) => { export const permissionDALFactory = (db: TDbClient) => {
const getOrgPermission = async (userId: string, orgId: string) => { const getOrgPermission = async (userId: string, orgId: string) => {
try { 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 const membership = await db
.replicaNode()(TableName.OrgMembership) .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) .where(`${TableName.OrgMembership}.orgId`, orgId)
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) .where(`${TableName.OrgMembership}.userId`, userId)
.select("permissions") .leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`)
.select(selectAllTableCols(TableName.OrgMembership)) .leftJoin<Awaited<typeof groupSubQuery>[0]>(
.first(); 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) { } catch (error) {
throw new DatabaseError({ error, name: "GetOrgPermission" }); throw new DatabaseError({ error, name: "GetOrgPermission" });
} }
@@ -47,74 +126,31 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => { const getProjectPermission = async (userId: string, projectId: string) => {
try { try {
const groups: string[] = await db const docs = await db
.replicaNode()(TableName.GroupProjectMembership) .replicaNode()(TableName.Users)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId) .where(`${TableName.Users}.id`, userId)
.pluck(`${TableName.GroupProjectMembership}.groupId`); .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
const groupDocs = await db void queryBuilder
.replicaNode()(TableName.UserGroupMembership) .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
.where(`${TableName.UserGroupMembership}.userId`, userId) .andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups) })
.join( .leftJoin(
TableName.GroupProjectMembership,
`${TableName.GroupProjectMembership}.groupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join(
TableName.GroupProjectMembershipRole, TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`, `${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id` `${TableName.GroupProjectMembership}.id`
) )
.leftJoin<TProjectRoles>(
.leftJoin( { groupCustomRoles: TableName.ProjectRoles },
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`, `${TableName.GroupProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id` `groupCustomRoles.id`
) )
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) .leftJoin(TableName.ProjectMembership, (queryBuilder) => {
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) void queryBuilder
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
})
.leftJoin( .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,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`, `${TableName.ProjectUserMembershipRole}.projectMembershipId`,
`${TableName.ProjectMembership}.id` `${TableName.ProjectMembership}.id`
@@ -124,176 +160,229 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.ProjectUserMembershipRole}.customRoleId`, `${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id` `${TableName.ProjectRoles}.id`
) )
.leftJoin( .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
TableName.ProjectUserAdditionalPrivilege, void queryBuilder
`${TableName.ProjectUserAdditionalPrivilege}.projectId`, .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
`${TableName.ProjectMembership}.projectId` .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
) })
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where(`${TableName.ProjectMembership}.userId`, userId)
.where(`${TableName.ProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
.select( .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("id").withSchema(TableName.ProjectMembership).as("membershipId"),
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
db.ref("projectId").withSchema(TableName.ProjectMembership), db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
db.ref("orgId").withSchema(TableName.Project), db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
db.ref("permissions").withSchema(TableName.ProjectRoles), db
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"), .ref("temporaryMode")
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"), .withSchema(TableName.ProjectUserMembershipRole)
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"), .as("userProjectMembershipRoleTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"), db
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"), .ref("isTemporary")
.withSchema(TableName.ProjectUserMembershipRole)
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"), .as("userProjectMembershipRoleIsTemporary"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"), 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 db
.ref("temporaryAccessStartTime") .ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege) .withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessStartTime"), .as("userAdditionalPrivilegesTemporaryAccessStartTime"),
db db
.ref("temporaryAccessEndTime") .ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege) .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, data: docs,
key: "projectId", key: "projectId",
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({ parentMapper: ({
orgId,
orgAuthEnforced,
membershipId,
groupMembershipId,
membershipCreatedAt,
groupMembershipCreatedAt,
groupMembershipUpdatedAt,
membershipUpdatedAt
}) => ({
orgId, orgId,
orgAuthEnforced, orgAuthEnforced,
userId, userId,
id: membershipId,
projectId, projectId,
createdAt: membershipCreatedAt, id: membershipId || groupMembershipId,
updatedAt: membershipUpdatedAt createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
}), }),
childrenMapper: [ childrenMapper: [
{ {
key: "id", key: "userGroupProjectMembershipRoleId",
label: "roles" as const, label: "userGroupRoles" as const,
mapper: (data) => mapper: ({
ProjectUserMembershipRolesSchema.extend({ userGroupProjectMembershipRoleId,
permissions: z.unknown(), userGroupProjectMembershipRole,
customRoleSlug: z.string().optional().nullable() userGroupProjectMembershipRolePermission,
}).parse(data) 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, label: "additionalPrivileges" as const,
mapper: ({ mapper: ({
userApId, userAdditionalPrivilegesId,
userApPermissions, userAdditionalPrivilegesPermissions,
userApIsTemporary, userAdditionalPrivilegesIsTemporary,
userApTemporaryMode, userAdditionalPrivilegesTemporaryMode,
userApTemporaryRange, userAdditionalPrivilegesTemporaryRange,
userApTemporaryAccessEndTime, userAdditionalPrivilegesTemporaryAccessEndTime,
userApTemporaryAccessStartTime userAdditionalPrivilegesTemporaryAccessStartTime
}) => ({ }) => ({
id: userApId, id: userAdditionalPrivilegesId,
permissions: userApPermissions, permissions: userAdditionalPrivilegesPermissions,
temporaryRange: userApTemporaryRange, temporaryRange: userAdditionalPrivilegesTemporaryRange,
temporaryMode: userApTemporaryMode, temporaryMode: userAdditionalPrivilegesTemporaryMode,
temporaryAccessEndTime: userApTemporaryAccessEndTime, temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
temporaryAccessStartTime: userApTemporaryAccessStartTime, temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
isTemporary: userApIsTemporary isTemporary: userAdditionalPrivilegesIsTemporary
}) })
} }
] ]
}); });
const groupPermission = groupDocs.length if (!userPermission) return undefined;
? sqlNestRelationships({ if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined;
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;
// when introducting cron mode change it here // when introducting cron mode change it here
const activeRoles = const activeRoles =
permission?.[0]?.roles?.filter( userPermission?.projecMembershiptRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? []; ) ?? [];
const activeGroupRoles = const activeGroupRoles =
groupPermission?.[0]?.roles?.filter( userPermission?.userGroupRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? []; ) ?? [];
const activeAdditionalPrivileges = const activeAdditionalPrivileges =
permission?.[0]?.additionalPrivileges?.filter( userPermission?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < 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 { return {
...(permission[0] || groupPermission[0]), ...userPermission,
roles: [...activeRoles, ...activeGroupRoles], roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges] additionalPrivileges: activeAdditionalPrivileges
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" }); throw new DatabaseError({ error, name: "GetProjectPermission" });
@@ -20,7 +20,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal"; import { TPermissionDALFactory } from "./permission-dal";
import { validateOrgSAML } from "./permission-fns"; import { validateOrgSAML } from "./permission-fns";
import { TBuildProjectPermissionDTO } from "./permission-types"; import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
import { import {
buildServiceTokenProjectPermission, buildServiceTokenProjectPermission,
projectAdminPermissions, projectAdminPermissions,
@@ -47,7 +47,9 @@ export const permissionServiceFactory = ({
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL
}: TPermissionServiceFactoryDep) => { }: TPermissionServiceFactoryDep) => {
const buildOrgPermission = (role: string, permission?: unknown) => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
const rules = orgUserRoles
.map(({ role, permissions }) => {
switch (role) { switch (role) {
case OrgMembershipRole.Admin: case OrgMembershipRole.Admin:
return orgAdminPermissions; return orgAdminPermissions;
@@ -56,17 +58,18 @@ export const permissionServiceFactory = ({
case OrgMembershipRole.NoAccess: case OrgMembershipRole.NoAccess:
return orgNoAccessPermissions; return orgNoAccessPermissions;
case OrgMembershipRole.Custom: case OrgMembershipRole.Custom:
return createMongoAbility<OrgPermissionSet>( return unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>( permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
permission as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
),
{
conditionsMatcher
}
); );
default: default:
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" }); throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
} }
})
.reduce((curr, prev) => prev.concat(curr), []);
return createMongoAbility<OrgPermissionSet>(rules, {
conditionsMatcher
});
}; };
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => { const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
@@ -129,7 +132,13 @@ export const permissionServiceFactory = ({
validateOrgSAML(authMethod, membership.orgAuthEnforced); 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) => { const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
@@ -138,7 +147,10 @@ export const permissionServiceFactory = ({
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" }); 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 ( const getOrgPermission = async (
@@ -169,11 +181,11 @@ export const permissionServiceFactory = ({
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
if (!orgRole) throw new BadRequestError({ message: "Role not found" }); if (!orgRole) throw new BadRequestError({ message: "Role not found" });
return { return {
permission: buildOrgPermission(OrgMembershipRole.Custom, orgRole.permissions), permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
role: orgRole role: orgRole
}; };
} }
return { permission: buildOrgPermission(role, []) }; return { permission: buildOrgPermission([{ role, permissions: [] }]) };
}; };
// user permission for a project in an organization // user permission for a project in an organization
@@ -2,3 +2,8 @@ export type TBuildProjectPermissionDTO = {
permissions?: unknown; permissions?: unknown;
role: string; role: string;
}[]; }[];
export type TBuildOrgPermissionDTO = {
permissions?: unknown;
role: string;
}[];
+24 -20
View File
@@ -7,7 +7,11 @@ import {
TScanFullRepoEventPayload, TScanFullRepoEventPayload,
TScanPushEventPayload TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; } 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 { export enum QueueName {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@@ -42,6 +46,7 @@ export enum QueueJobs {
SecWebhook = "secret-webhook-trigger", SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats", TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull", IntegrationSync = "secret-integration-pull",
SendFailedIntegrationSyncEmails = "send-failed-integration-sync-emails",
SecretScan = "secret-scan", SecretScan = "secret-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost-job", UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
DynamicSecretRevocation = "dynamic-secret-revocation", DynamicSecretRevocation = "dynamic-secret-revocation",
@@ -88,17 +93,25 @@ export type TQueueJobTypes = {
name: QueueJobs.SecWebhook; name: QueueJobs.SecWebhook;
payload: { projectId: string; environment: string; secretPath: string; depth?: number }; payload: { projectId: string; environment: string; secretPath: string; depth?: number };
}; };
[QueueName.IntegrationSync]: {
name: QueueJobs.IntegrationSync; [QueueName.AccessTokenStatusUpdate]:
payload: { | {
isManual?: boolean; name: QueueJobs.IdentityAccessTokenStatusUpdate;
actorId?: string; payload: { identityAccessTokenId: string; numberOfUses: number };
projectId: string; }
environment: string; | {
secretPath: string; name: QueueJobs.ServiceTokenStatusUpdate;
depth?: number; payload: { serviceTokenId: string };
deDupeQueue?: Record<string, boolean>;
}; };
[QueueName.IntegrationSync]:
| {
name: QueueJobs.IntegrationSync;
payload: TIntegrationSyncPayload;
}
| {
name: QueueJobs.SendFailedIntegrationSyncEmails;
payload: TFailedIntegrationSyncEmailsPayload;
}; };
[QueueName.SecretFullRepoScan]: { [QueueName.SecretFullRepoScan]: {
name: QueueJobs.SecretScan; name: QueueJobs.SecretScan;
@@ -153,15 +166,6 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration; name: QueueJobs.ProjectV3Migration;
payload: { projectId: string }; payload: { projectId: string };
}; };
[QueueName.AccessTokenStatusUpdate]:
| {
name: QueueJobs.IdentityAccessTokenStatusUpdate;
payload: { identityAccessTokenId: string; numberOfUses: number };
}
| {
name: QueueJobs.ServiceTokenStatusUpdate;
payload: { serviceTokenId: string };
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import fastifyPlugin from "fastify-plugin"; import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { import {
@@ -11,6 +12,12 @@ import {
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
enum JWTErrors {
JwtExpired = "jwt expired",
JwtMalformed = "jwt malformed",
InvalidAlgorithm = "invalid algorithm"
}
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => { export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
server.setErrorHandler((error, req, res) => { server.setErrorHandler((error, req, res) => {
req.log.error(error); req.log.error(error);
@@ -36,6 +43,27 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
status: error.status, status: error.status,
detail: error.detail 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 { } else {
void res.send(error); void res.send(error);
} }
@@ -18,7 +18,7 @@ import {
infisicalSymmetricDecrypt, infisicalSymmetricDecrypt,
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } 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 { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -68,12 +68,12 @@ export const identityOidcAuthServiceFactory = ({
identityId: identityOidcAuth.identityId identityId: identityOidcAuth.identityId
}); });
if (!identityMembershipOrg) { 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 }); const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) { 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({ const key = infisicalSymmetricDecrypt({
@@ -106,7 +106,7 @@ export const identityOidcAuthServiceFactory = ({
const decodedToken = jwt.decode(oidcJwt, { complete: true }); const decodedToken = jwt.decode(oidcJwt, { complete: true });
if (!decodedToken) { if (!decodedToken) {
throw new BadRequestError({ throw new UnauthorizedError({
message: "Invalid JWT" message: "Invalid JWT"
}); });
} }
@@ -119,13 +119,24 @@ export const identityOidcAuthServiceFactory = ({
const { kid } = decodedToken.header; const { kid } = decodedToken.header;
const oidcSigningKey = await client.getSigningKey(kid); const oidcSigningKey = await client.getSigningKey(kid);
const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), { let tokenData: Record<string, string>;
try {
tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer issuer: identityOidcAuth.boundIssuer
}) as Record<string, string>; }) as Record<string, string>;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new UnauthorizedError({
message: `Access denied: ${error.message}`
});
}
throw error;
}
if (identityOidcAuth.boundSubject) { if (identityOidcAuth.boundSubject) {
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) { if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
throw new ForbiddenRequestError({ throw new UnauthorizedError({
message: "Access denied: OIDC subject not allowed." message: "Access denied: OIDC subject not allowed."
}); });
} }
@@ -137,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({
.split(", ") .split(", ")
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue)) .some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
) { ) {
throw new ForbiddenRequestError({ throw new UnauthorizedError({
message: "Access denied: OIDC audience not allowed." message: "Access denied: OIDC audience not allowed."
}); });
} }
@@ -150,7 +161,7 @@ export const identityOidcAuthServiceFactory = ({
if ( if (
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry)) !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
) { ) {
throw new ForbiddenRequestError({ throw new UnauthorizedError({
message: "Access denied: OIDC claim not allowed." message: "Access denied: OIDC claim not allowed."
}); });
} }
+6 -6
View File
@@ -60,7 +60,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
name: "Admin", name: "Admin",
slug: "admin", slug: "admin",
description: "Complete administration access over the organization", description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions.rules), permissions: packRules(orgAdminPermissions),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
}; };
@@ -72,7 +72,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
name: "Member", name: "Member",
slug: "member", slug: "member",
description: "Non-administrative role in an organization", description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions.rules), permissions: packRules(orgMemberPermissions),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
}; };
@@ -84,7 +84,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
name: "No Access", name: "No Access",
slug: "no-access", slug: "no-access",
description: "No access to any resources in the organization", description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions.rules), permissions: packRules(orgNoAccessPermissions),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
}; };
@@ -151,7 +151,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
name: "Admin", name: "Admin",
slug: "admin", slug: "admin",
description: "Complete administration access over the organization", description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions.rules), permissions: packRules(orgAdminPermissions),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
}, },
@@ -161,7 +161,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
name: "Member", name: "Member",
slug: "member", slug: "member",
description: "Non-administrative role in an organization", description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions.rules), permissions: packRules(orgMemberPermissions),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
}, },
@@ -171,7 +171,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
name: "No Access", name: "No Access",
slug: "no-access", slug: "no-access",
description: "No access to any resources in the organization", description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions.rules), permissions: packRules(orgNoAccessPermissions),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
}, },
@@ -26,7 +26,10 @@ export const getBotKeyFnFactory = (
) => { ) => {
const getBotKeyFn = async (projectId: string) => { const getBotKeyFn = async (projectId: string) => {
const project = await projectDAL.findById(projectId); 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) { if (project.version === 3) {
return { project, shouldUseSecretV2Bridge: true }; return { project, shouldUseSecretV2Bridge: true };
@@ -545,7 +545,11 @@ export const secretImportServiceFactory = ({
return importedSecrets; 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 }); const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
return importedSecrets.map((el) => ({ return importedSecrets.map((el) => ({
+10 -2
View File
@@ -832,7 +832,11 @@ export const createManySecretsRawFnFactory = ({
secretDAL 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 inputSecrets = secrets.map((secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
@@ -993,7 +997,11 @@ export const updateManySecretsRawFnFactory = ({
return updatedSecrets; 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 }); const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" }); if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
+97 -7
View File
@@ -1,7 +1,13 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { AxiosError } from "axios"; 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 { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types"; 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"; 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 { interpolateSecrets } from "./secret-fns";
import { import {
TCreateSecretReminderDTO, TCreateSecretReminderDTO,
TFailedIntegrationSyncEmailsPayload,
THandleReminderDTO, THandleReminderDTO,
TIntegrationSyncPayload,
TRemoveSecretReminderDTO, TRemoveSecretReminderDTO,
TSyncSecretsDTO TSyncSecretsDTO
} from "./secret-types"; } 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) => { queueService.start(QueueName.SecretSync, async (job) => {
const { const {
_deDupeQueue: deDupeQueue, _deDupeQueue: deDupeQueue,
@@ -554,7 +575,46 @@ export const secretQueueFactory = ({
}); });
queueService.start(QueueName.IntegrationSync, async (job) => { queueService.start(QueueName.IntegrationSync, async (job) => {
const { environment, actorId, isManual, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data; if (job.name === QueueJobs.SendFailedIntegrationSyncEmails) {
const appCfg = getConfig();
const jobPayload = job.data as TFailedIntegrationSyncEmailsPayload;
const projectMembers = await projectMembershipDAL.findAllProjectMembers(jobPayload.projectId);
const project = await projectDAL.findById(jobPayload.projectId);
// 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
);
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}`
}
});
}
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 (depth > MAX_SYNC_SECRET_DEPTH) return;
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@@ -577,7 +637,7 @@ export const secretQueueFactory = ({
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string); const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
logger.info( 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}]` `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( await Promise.all(
imports imports
@@ -629,7 +689,7 @@ export const secretQueueFactory = ({
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds); const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string); const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
logger.info( 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}]` `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( await Promise.all(
referencedFolderIds referencedFolderIds
@@ -663,9 +723,11 @@ export const secretQueueFactory = ({
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) ({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
); );
const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = [];
if (!integrations.length) return; if (!integrations.length) return;
logger.info( 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}]` `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( const lock = await keyStore.acquireLock(
@@ -688,7 +750,7 @@ export const secretQueueFactory = ({
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
if (isStaleSyncIntegration) { if (isStaleSyncIntegration) {
logger.info( 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}]` `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
); );
return; return;
} }
@@ -833,10 +895,18 @@ export const secretQueueFactory = ({
syncMessage: response?.syncMessage ?? "", syncMessage: response?.syncMessage ?? "",
isSynced: response?.isSynced ?? true 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) { } catch (err) {
logger.error( logger.error(
err, err,
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]` `Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]`
); );
const message = const message =
@@ -863,10 +933,29 @@ export const secretQueueFactory = ({
syncMessage: message, syncMessage: message,
isSynced: false isSynced: false
}); });
integrationsFailedToSync.push({
integrationId: integration.id,
syncMessage: message
});
} }
} }
} finally { } finally {
await lock.release(); 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
});
}
} }
await keyStore.setItemWithExpiry( await keyStore.setItemWithExpiry(
@@ -875,6 +964,7 @@ export const secretQueueFactory = ({
lockAcquiredTime.toISOString() lockAcquiredTime.toISOString()
); );
logger.info("Secret integration sync ended: %s", job.id); logger.info("Secret integration sync ended: %s", job.id);
}
}); });
queueService.start(QueueName.SecretReminder, async ({ data }) => { queueService.start(QueueName.SecretReminder, async ({ data }) => {
+47 -10
View File
@@ -1101,7 +1101,11 @@ export const secretServiceFactory = ({
return { secrets, imports }; 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({ const { secrets, imports } = await getSecrets({
actorId, actorId,
@@ -1262,7 +1266,10 @@ export const secretServiceFactory = ({
}); });
if (!botKey) 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); const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) { if (expandSecretReferences) {
@@ -1354,7 +1361,11 @@ export const secretServiceFactory = ({
return { secret, type: SecretProtectionType.Direct as const }; 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 secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
@@ -1492,7 +1503,11 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secret }; 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 secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
@@ -1614,7 +1629,11 @@ export const secretServiceFactory = ({
}); });
return { type: SecretProtectionType.Direct as const, secret }; 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) { if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
policy, policy,
@@ -1714,7 +1733,11 @@ export const secretServiceFactory = ({
return { secrets, type: SecretProtectionType.Direct as const }; 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( const sanitizedSecrets = inputSecrets.map(
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => { ({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
@@ -1836,7 +1859,11 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secrets }; 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( const sanitizedSecrets = inputSecrets.map(
({ ({
secretComment, secretComment,
@@ -1964,7 +1991,11 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secrets }; 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) { if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
@@ -2298,7 +2329,10 @@ export const secretServiceFactory = ({
} }
if (!botKey) 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) => { await secretDAL.transaction(async (tx) => {
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx); const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
@@ -2381,7 +2415,10 @@ export const secretServiceFactory = ({
const { botKey } = await projectBotService.getBotKey(project.id); const { botKey } = await projectBotService.getBotKey(project.id);
if (!botKey) { 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); const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
@@ -1,4 +1,5 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { z } from "zod";
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
@@ -21,6 +22,29 @@ type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secret
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id">; type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id">;
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<typeof FailedIntegrationSyncEmailsPayloadSchema>;
export type TIntegrationSyncPayload = {
isManual?: boolean;
actorId?: string;
projectId: string;
environment: string;
secretPath: string;
depth?: number;
deDupeQueue?: Record<string, boolean>;
};
export type TCreateSecretDTO = { export type TCreateSecretDTO = {
secretName: string; secretName: string;
path: string; path: string;
+2 -1
View File
@@ -33,7 +33,8 @@ export enum SmtpTemplates {
SecretLeakIncident = "secretLeakIncident.handlebars", SecretLeakIncident = "secretLeakIncident.handlebars",
WorkspaceInvite = "workspaceInvitation.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars", ScimUserProvisioned = "scimUserProvisioned.handlebars",
PkiExpirationAlert = "pkiExpirationAlert.handlebars" PkiExpirationAlert = "pkiExpirationAlert.handlebars",
IntegrationSyncFailed = "integrationSyncFailed.handlebars"
} }
export enum SmtpHost { export enum SmtpHost {
@@ -0,0 +1,31 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Integration Sync Failed</title>
</head>
<body>
<h2>Infisical</h2>
<div>
<p>{{count}} integration(s) failed to sync.</p>
<a href="{{integrationUrl}}">
View your project integrations.
</a>
</div>
<br />
<div>
<p><strong>Project</strong>: {{projectName}}</p>
<p><strong>Environment</strong>: {{environment}}</p>
<p><strong>Secret Path</strong>: {{secretPath}}</p>
</div>
{{#if syncMessage}}
<p><b>Reason: </b>{{syncMessage}}</p>
{{/if}}
</body>
</html>
+14 -1
View File
@@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"strings" "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.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) { rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
silent, err := cmd.Flags().GetBool("silent") silent, err := cmd.Flags().GetBool("silent")
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
if !util.IsRunningInDocker() && !silent { if !util.IsRunningInDocker() && !silent {
util.CheckForUpdate() 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 // if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
+1 -1
View File
@@ -160,7 +160,7 @@ var secretsSetCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
if (token == nil) { if token == nil {
util.RequireLocalWorkspaceFile() util.RequireLocalWorkspaceFile()
} }
+1
View File
@@ -65,6 +65,7 @@ type DynamicSecretLease struct {
type TokenDetails struct { type TokenDetails struct {
Type string Type string
Token string Token string
Source string
} }
type SingleFolder struct { type SingleFolder struct {
+6
View File
@@ -87,11 +87,15 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
return nil, err 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. 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) 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. 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) infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME)
} }
} }
@@ -103,12 +107,14 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
return &models.TokenDetails{ return &models.TokenDetails{
Type: SERVICE_TOKEN_IDENTIFIER, Type: SERVICE_TOKEN_IDENTIFIER,
Token: infisicalToken, Token: infisicalToken,
Source: source,
}, nil }, nil
} }
return &models.TokenDetails{ return &models.TokenDetails{
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER, Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
Token: infisicalToken, Token: infisicalToken,
Source: source,
}, nil }, nil
} }
@@ -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
<Steps>
<Step>
Login to [Microsoft Entra ID](https://entra.microsoft.com/)
</Step>
<Step>
Go to Overview, Copy and store `Tenant Id`
![Copy Tenant Id](../../../images/platform/dynamic-secrets/dynamic-secret-ad-tenant-id.png)
</Step>
<Step>
Go to Applications > App registrations. Click on New Registration.
![Copy Tenant Id](../../../images/platform/dynamic-secrets/dynamic-secret-ad-new-registration.png)
</Step>
<Step>
Enter an application name. Click Register.
</Step>
<Step>
Copy and store `Application Id`.
![Copy Application Id](../../../images/platform/dynamic-secrets/dynamic-secret-ad-copy-app-id.png)
</Step>
<Step>
Go to Clients and Secrets. Click on New Client Secret.
</Step>
<Step>
Enter a description, select expiry and click Add.
</Step>
<Step>
Copy and store `Client Secret` value.
![Copy client Secret](../../../images/platform/dynamic-secrets/dynamic-secret-ad-add-client-secret.png)
</Step>
<Step>
Go to API Permissions. Click on Add a permission.
![Click add a permission](../../../images/platform/dynamic-secrets/dynamic-secret-ad-add-permission.png)
</Step>
<Step>
Click on Microsoft Graph.
![Click Microsoft Graph](../../../images/platform/dynamic-secrets/dynamic-secret-ad-select-graph.png)
</Step>
<Step>
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)
</Step>
<Step>
Click on Grant admin consent for app. Click yes to confirm.
![Grant admin consent](../../../images/platform/dynamic-secrets/dynamic-secret-ad-admin-consent.png)
</Step>
<Step>
Go to Dashboard. Click on show more.
![Show more](../../../images/platform/dynamic-secrets/dynamic-secret-ad-show-more.png)
</Step>
<Step>
Click on Roles & admins. Search for User Administrator and click on it.
![User Administrator](../../../images/platform/dynamic-secrets/dynamic-secret-ad-user-admin.png)
</Step>
<Step>
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)
</Step>
</Steps>
## Set up Dynamic Secrets with Azure Entra ID
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'Azure Entra ID'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ad-modal.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Prefix" type="string" required>
Prefix for the secrets to be created
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="Tenant ID" type="string" required>
The Tenant ID of your Azure Entra ID account.
</ParamField>
<ParamField path="Application ID" type="string" required>
The Application ID of the application you created in Azure Entra ID.
</ParamField>
<ParamField path="Client Secret" type="string" required>
The Client Secret of the application you created in Azure Entra ID.
</ParamField>
<ParamField path="Users" type="selection" required>
Multi select list of users to generate secrets for.
</ParamField>
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secrets for each user created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
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)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
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)
</Step>
</Steps>
## 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)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>
@@ -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. 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
<Tabs> <Tabs>
<Tab title="Infisical Cloud"> <Tab title="Infisical Cloud">
## Create Slack workflow integration ### Create Slack workflow integration
<Steps> <Steps>
<Step title="Navigate to the Workflow Integrations tab in your organization settings"> <Step title="Navigate to the Workflow Integrations tab in your organization settings">
In order to use Slack integration in your projects, you will first have to 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
</Steps> </Steps>
## Configure project to use Slack workflow integration ### Configure project to use Slack workflow integration
<Steps> <Steps>
<Step title="Navigate to the Workflow Integrations tab in the project settings"> <Step title="Navigate to the Workflow Integrations tab in the project settings">
@@ -56,7 +58,7 @@ This guide will provide step by step instructions on how to configure Slack inte
</Tab> </Tab>
<Tab title="Self-hosted setup"> <Tab title="Self-hosted setup">
## Configure admin settings ### Configure admin settings
Note that this step only has to be done once for the entire instance. Note that this step only has to be done once for the entire instance.
<Steps> <Steps>
@@ -90,7 +92,7 @@ This guide will provide step by step instructions on how to configure Slack inte
</Steps> </Steps>
## Create Slack workflow integration ### Create Slack workflow integration
<Steps> <Steps>
<Step title="Navigate to the Workflow Integrations tab in your organization settings"> <Step title="Navigate to the Workflow Integrations tab in your organization settings">
@@ -116,7 +118,7 @@ This guide will provide step by step instructions on how to configure Slack inte
</Steps> </Steps>
## Configure project to use Slack workflow integration ### Configure project to use Slack workflow integration
<Steps> <Steps>
<Step title="Navigate to the Workflow Integrations tab in the project settings"> <Step title="Navigate to the Workflow Integrations tab in the project settings">
@@ -140,3 +142,23 @@ This guide will provide step by step instructions on how to configure Slack inte
</Tab> </Tab>
</Tabs> </Tabs>
## Using the Slack integration in your private channels
<Steps>
<Step title="In the Apps section on Slack, find the Infisical app and view the app details">
![private slack setup
menu](/images/platform/workflow-integrations/slack-integration/private-slack-setup-menu.png)
</Step>
<Step title="Select Add this app to a channel">
![private slack setup
add](/images/platform/workflow-integrations/slack-integration/private-slack-setup-add.png)
</Step>
<Step title="Find the private channel you want to setup notifications for and press Add">
![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)
</Step>
</Steps>
Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+2 -1
View File
@@ -167,7 +167,8 @@
"documentation/platform/dynamic-secrets/rabbit-mq", "documentation/platform/dynamic-secrets/rabbit-mq",
"documentation/platform/dynamic-secrets/aws-iam", "documentation/platform/dynamic-secrets/aws-iam",
"documentation/platform/dynamic-secrets/mongo-atlas", "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"
] ]
}, },
{ {
+4
View File
@@ -1,9 +1,11 @@
--- ---
title: "Infisical Node.js SDK" title: "Infisical Node.js SDK"
sidebarTitle: "Node.js" sidebarTitle: "Node.js"
url: "https://github.com/Infisical/node-sdk-v2"
icon: "node" 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. 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) - [NPM Package](https://www.npmjs.com/package/@infisical/sdk)
@@ -552,3 +554,5 @@ const decryptedString = await client.decryptSymmetric({
#### Returns (string) #### Returns (string)
`plaintext` (string): The decrypted plaintext. `plaintext` (string): The decrypted plaintext.
*/}
+1 -1
View File
@@ -10,7 +10,7 @@ From local development to production, Infisical SDKs provide the easiest way for
- Fetch secrets on demand - Fetch secrets on demand
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Node" href="/sdks/languages/node" icon="node" color="#68a063"> <Card title="Node" href="https://github.com/Infisical/node-sdk-v2" icon="node" color="#68a063">
Manage secrets for your Node application on demand Manage secrets for your Node application on demand
</Card> </Card>
<Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe"> <Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe">
+19 -11
View File
@@ -1,5 +1,5 @@
{ {
"name": "frontend", "name": "relock-npm-lock-v2-SvMQeF",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -65,7 +65,7 @@
"i18next-http-backend": "^2.2.0", "i18next-http-backend": "^2.2.0",
"infisical-node": "^1.0.37", "infisical-node": "^1.0.37",
"ip": "^2.0.1", "ip": "^2.0.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.2",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0", "lottie-react": "^2.4.0",
@@ -12729,9 +12729,10 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.4.7", "version": "2.5.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz",
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==", "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true "optional": true
}, },
"node_modules/domutils": { "node_modules/domutils": {
@@ -16873,22 +16874,29 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/jspdf": { "node_modules/jspdf": {
"version": "2.5.1", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
"integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.14.0", "@babel/runtime": "^7.23.2",
"atob": "^2.1.2", "atob": "^2.1.2",
"btoa": "^1.2.1", "btoa": "^1.2.1",
"fflate": "^0.4.8" "fflate": "^0.8.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"canvg": "^3.0.6", "canvg": "^3.0.6",
"core-js": "^3.6.0", "core-js": "^3.6.0",
"dompurify": "^2.2.0", "dompurify": "^2.5.4",
"html2canvas": "^1.0.0-rc.5" "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": { "node_modules/jsprim": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
+1 -1
View File
@@ -73,7 +73,7 @@
"i18next-http-backend": "^2.2.0", "i18next-http-backend": "^2.2.0",
"infisical-node": "^1.0.37", "infisical-node": "^1.0.37",
"ip": "^2.0.1", "ip": "^2.0.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.2",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0", "lottie-react": "^2.4.0",
@@ -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 }) => (
<div>
<FormLabel
label={label}
icon={
<Tooltip
content={
<span>
{content}{" "}
<a
href={linkToMore}
target="_blank"
rel="noopener noreferrer"
className="text-primary-700"
>
More
</a>
</span>
}
>
<FontAwesomeIcon
icon={faQuestionCircle}
size="sm"
className="relative bottom-1 right-1"
/>
</Tooltip>
}
/>
</div>
);
@@ -1,36 +1,12 @@
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FormLabelToolTip } from "./FormLabelToolTip";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormLabel, Tooltip } from "../v2";
// To give users example of possible values of TTL // To give users example of possible values of TTL
export const TtlFormLabel = ({ label }: { label: string }) => ( export const TtlFormLabel = ({ label }: { label: string }) => (
<div> <div>
<FormLabel <FormLabelToolTip
label={label} label={label}
icon={ content="1m, 2h, 3d. "
<Tooltip linkToMore="https://github.com/vercel/ms?tab=readme-ov-file#examples"
content={
<span>
1m, 2h, 3d.{" "}
<a
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
target="_blank"
rel="noopener noreferrer"
className="text-primary-700"
>
More
</a>
</span>
}
>
<FontAwesomeIcon
icon={faQuestionCircle}
size="sm"
className="relative bottom-1 right-1"
/>
</Tooltip>
}
/> />
</div> </div>
); );
@@ -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 = ({ export const useGetDynamicSecretsOfAllEnv = ({
path, path,
projectSlug, projectSlug,
+12 -1
View File
@@ -24,7 +24,8 @@ export enum DynamicSecretProviders {
MongoAtlas = "mongo-db-atlas", MongoAtlas = "mongo-db-atlas",
ElasticSearch = "elastic-search", ElasticSearch = "elastic-search",
MongoDB = "mongo-db", MongoDB = "mongo-db",
RabbitMq = "rabbit-mq" RabbitMq = "rabbit-mq",
AzureEntraId = "azure-entra-id"
} }
export enum SqlProviders { export enum SqlProviders {
@@ -177,6 +178,16 @@ export type TDynamicSecretProvider =
}; };
ca?: string; ca?: string;
}; };
}
| {
type: DynamicSecretProviders.AzureEntraId;
inputs: {
tenantId: string;
userId: string;
email: string;
applicationId: string;
clientSecret: string;
};
}; };
export type TCreateDynamicSecretDTO = { export type TCreateDynamicSecretDTO = {
@@ -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<typeof formSchema>;
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<TForm>({
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 (
<div>
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Prefix"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="defaultTTL"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="maxTTL"
defaultValue="24h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Max TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<Link href="https://infisical.com/docs/documentation/platform/dynamic-secrets/azure-entra-id" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
<div className="flex flex-col">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="provider.tenantId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tenant Id"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Tenant Id from Azure Entra ID App installation" />
</FormControl>
)}
/>
</div>
</div>
<div className="flex flex-col">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="provider.applicationId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Application Id"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Application ID from Azure Entra ID App installation" />
</FormControl>
)}
/>
</div>
</div>
<div className="flex flex-col">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="provider.clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Client Secret from Azure Entra ID App installation" />
</FormControl>
)}
/>
</div>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Select Users
</div>
<div className="mb-4 flex items-center text-sm font-normal text-mineshaft-400">
&nbsp; We create a unique dynamic secret for each user in Entra Id.
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-4">
<Controller
control={control}
name="selectedUsers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu >
<DropdownMenuTrigger
className="w-72"
disabled={loading || errored || !configurationComplete}
>
<Tooltip
hidden={!loading && !errored && configurationComplete}
content=
{
<div>
{(() => {
let icon;
if (errored) {
icon = <FontAwesomeIcon icon={faWarning} color="red" />;
} else if (loading || !configurationComplete) {
icon = <FontAwesomeIcon icon={faWarning} color="yellow" />;
} else {
icon = null;
}
return icon;
})()}
<span className="ml-4 cursor-default text-mineshaft-300 hover:text-mineshaft-200">
{(() => {
let message;
if (loading) {
message = "Loading, please wait...";
} else if (errored) {
message = "Check the configuration";
} else if (!configurationComplete) {
message = "Configuration incomplete";
} else {
message = ""; // or you can leave it undefined
}
return message;
})()}
</span>
</div>
}
>
<div>
<Input
isReadOnly
value={value?.length ? `${value.length} selected` : ""}
className={`text-left ${loading || errored || !configurationComplete ? "cursor-not-allowed" : ""}`}
placeholder="Select users"
/>
</div>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align="start"
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
>
{data && data.map((user) => {
const ids = value?.map((selectedUser) => selectedUser.id)
const isChecked = ids?.includes(user.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el) => el.id !== user.id)
: [...(value || []), user]
);
}}
key={`create-policy-members-${user.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{user.name} <br /> {`(${user.email})`}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isLoading || isError}>
Submit
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
</div>
);
};
@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { DiRedis } from "react-icons/di"; 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 { faAws } from "@fortawesome/free-brands-svg-icons";
import { faDatabase } from "@fortawesome/free-solid-svg-icons"; import { faDatabase } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -11,6 +11,7 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm"; import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
import { AwsIamInputForm } from "./AwsIamInputForm"; import { AwsIamInputForm } from "./AwsIamInputForm";
import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm";
import { CassandraInputForm } from "./CassandraInputForm"; import { CassandraInputForm } from "./CassandraInputForm";
import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
@@ -77,6 +78,11 @@ const DYNAMIC_SECRET_LIST = [
icon: <SiRabbitmq size="1.5rem" />, icon: <SiRabbitmq size="1.5rem" />,
provider: DynamicSecretProviders.RabbitMq, provider: DynamicSecretProviders.RabbitMq,
title: "RabbitMQ" title: "RabbitMQ"
},
{
icon: <SiMicrosoftazure size="1.5rem" />,
provider: DynamicSecretProviders.AzureEntraId,
title: "Azure Entra ID",
} }
]; ];
@@ -300,6 +306,25 @@ export const CreateDynamicSecretForm = ({
/> />
</motion.div> </motion.div>
)} )}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === DynamicSecretProviders.AzureEntraId && (
<motion.div
key="dynamic-azure-entra-id-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<AzureEntraIdInputForm
onCompleted={handleFormReset}
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
/>
</motion.div>
)
}
</AnimatePresence> </AnimatePresence>
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -176,6 +176,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
); );
} }
if (provider === DynamicSecretProviders.AzureEntraId) {
const { email, password } = data as {
email: string;
password: string;
};
return (
<div>
<OutputDisplay label="Email" value={email} />
<OutputDisplay
label="Password"
value={password}
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal."
/>
</div>
);
}
return null; return null;
}; };
@@ -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<typeof formSchema>;
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<TForm>({
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 (
<div>
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="newName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="defaultTTL"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="maxTTL"
defaultValue="24h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Max TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.email"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Email"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
value={field.value}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.userId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User ID"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
isReadOnly
value={field.value}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.tenantId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tenant ID"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
isReadOnly
value={field.value}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.applicationId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Application ID"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</div>
);
};
@@ -6,6 +6,7 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { EditDynamicSecretAwsElastiCacheProviderForm } from "./EditDynamicSecretAwsElastiCacheProviderForm"; import { EditDynamicSecretAwsElastiCacheProviderForm } from "./EditDynamicSecretAwsElastiCacheProviderForm";
import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm"; import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm";
import { EditDynamicSecretAzureEntraIdForm } from "./EditDynamicSecretAzureEntraIdForm";
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm"; import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm"; import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm";
import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm";
@@ -202,6 +203,24 @@ export const EditDynamicSecretForm = ({
/> />
</motion.div> </motion.div>
)} )}
{dynamicSecretDetails?.type === DynamicSecretProviders.AzureEntraId && (
<motion.div
key="azure-entra-id-edit"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<EditDynamicSecretAzureEntraIdForm
onClose={onClose}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecret={dynamicSecretDetails}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence> </AnimatePresence>
); );
}; };
@@ -66,12 +66,11 @@ export const ManagePlansTable = ({ billingCycle }: Props) => {
{subscription && {subscription &&
!isTableDataLoading && !isTableDataLoading &&
tableData && tableData &&
tableData.rows.map(({ name, starter, team, pro, enterprise }) => { tableData.rows.map(({ name, starter, pro, enterprise }) => {
return ( return (
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}> <Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
<Td>{displayCell(name)}</Td> <Td>{displayCell(name)}</Td>
<Td className="text-center">{displayCell(starter)}</Td> <Td className="text-center">{displayCell(starter)}</Td>
<Td className="text-center">{displayCell(team)}</Td>
<Td className="text-center">{displayCell(pro)}</Td> <Td className="text-center">{displayCell(pro)}</Td>
<Td className="text-center">{displayCell(enterprise)}</Td> <Td className="text-center">{displayCell(enterprise)}</Td>
</Tr> </Tr>
@@ -5,6 +5,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api"; import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api";
const slackFormSchema = z.object({ const slackFormSchema = z.object({
@@ -65,6 +66,8 @@ export const IntegrationPanel = () => {
const { data: adminSlackConfig } = useGetAdminSlackConfig(); const { data: adminSlackConfig } = useGetAdminSlackConfig();
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
useEffect(() => { useEffect(() => {
if (adminSlackConfig) { if (adminSlackConfig) {
@@ -120,6 +123,9 @@ export const IntegrationPanel = () => {
<Input <Input
{...field} {...field}
value={field.value || ""} value={field.value || ""}
type={isSlackClientIdFocused ? "text" : "password"}
onFocus={() => setIsSlackClientIdFocused.on()}
onBlur={() => setIsSlackClientIdFocused.off()}
onChange={(e) => field.onChange(e.target.value)} onChange={(e) => field.onChange(e.target.value)}
/> />
</FormControl> </FormControl>
@@ -138,6 +144,9 @@ export const IntegrationPanel = () => {
<Input <Input
{...field} {...field}
value={field.value || ""} value={field.value || ""}
type={isSlackClientSecretFocused ? "text" : "password"}
onFocus={() => setIsSlackClientSecretFocused.on()}
onBlur={() => setIsSlackClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)} onChange={(e) => field.onChange(e.target.value)}
/> />
</FormControl> </FormControl>