Merge remote-tracking branch 'origin/main' into misc/terraform-project-group-prereq

This commit is contained in:
Sheen Capadngan
2024-09-23 15:09:56 +08:00
166 changed files with 7249 additions and 1987 deletions
-3
View File
@@ -72,6 +72,3 @@ PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS= PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY= SSL_CLIENT_CERTIFICATE_HEADER_KEY=
WORKFLOW_SLACK_CLIENT_ID=
WORKFLOW_SLACK_CLIENT_SECRET=
+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",
@@ -237,7 +270,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const dynamicSecretCfgs = await server.services.dynamicSecret.list({ const dynamicSecretCfgs = await server.services.dynamicSecret.listDynamicSecretsByEnv({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -101,6 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid" message: "Slug must be a valid"
}), }),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}), }),
response: { response: {
+23 -11
View File
@@ -87,6 +87,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
/*
* Daniel: This endpoint is no longer is use.
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
*
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
*/
server.route({ server.route({
method: "GET", method: "GET",
url: "/:workspaceId/audit-logs", url: "/:workspaceId/audit-logs",
@@ -101,7 +107,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId) workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
}), }),
querystring: z.object({ querystring: z.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
@@ -122,10 +128,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}) })
.merge( .merge(
z.object({ z.object({
project: z.object({ project: z
name: z.string(), .object({
slug: z.string() name: z.string(),
}), slug: z.string()
})
.optional(),
event: z.object({ event: z.object({
type: z.string(), type: z.string(),
metadata: z.any() metadata: z.any()
@@ -146,12 +154,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId, actor: req.permission.type,
...req.query,
endDate: req.query.endDate, filter: {
startDate: req.query.startDate || getLastMidnightDateISO(), ...req.query,
auditLogActor: req.query.actor, projectId: req.params.workspaceId,
actor: req.permission.type endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined
}
}); });
return { auditLogs }; return { auditLogs };
} }
@@ -3,9 +3,12 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { AuditLogsSchema, TableName } from "@app/db/schemas"; import { AuditLogsSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { EventType } from "./audit-log-types";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>; export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@@ -25,37 +28,81 @@ export const auditLogDALFactory = (db: TDbClient) => {
const auditLogOrm = ormify(db, TableName.AuditLog); const auditLogOrm = ormify(db, TableName.AuditLog);
const find = async ( const find = async (
{ orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery, {
orgId,
projectId,
userAgentType,
startDate,
endDate,
limit = 20,
offset = 0,
actorId,
actorType,
eventType,
eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
tx?: Knex tx?: Knex
) => { ) => {
if (!orgId && !projectId) {
throw new Error("Either orgId or projectId must be provided");
}
try { try {
// Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog) const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
.where(
stripUndefinedInWhere({
projectId,
[`${TableName.AuditLog}.orgId`]: orgId,
eventType,
userAgentType
})
)
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`) .leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
// eslint-disable-next-line func-names
.where(function () {
if (orgId) {
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId);
} else if (projectId) {
void this.where(`${TableName.AuditLog}.projectId`, projectId);
}
});
if (userAgentType) {
void sqlQuery.where("userAgentType", userAgentType);
}
// Select statements
void sqlQuery
.select(selectAllTableCols(TableName.AuditLog)) .select(selectAllTableCols(TableName.AuditLog))
.select( .select(
db.ref("name").withSchema(TableName.Project).as("projectName"), db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug") db.ref("slug").withSchema(TableName.Project).as("projectSlug")
) )
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
.orderBy(`${TableName.AuditLog}.createdAt`, "desc"); .orderBy(`${TableName.AuditLog}.createdAt`, "desc");
if (actor) { // Special case: Filter by actor ID
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]); if (actorId) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
} }
// Special case: Filter by key/value pairs in eventMetadata field
if (eventMetadata && Object.keys(eventMetadata).length) {
Object.entries(eventMetadata).forEach(([key, value]) => {
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
});
}
// Filter by actor type
if (actorType) {
void sqlQuery.where("actor", actorType);
}
// Filter by event types
if (eventType?.length) {
void sqlQuery.whereIn("eventType", eventType);
}
// Filter by date range
if (startDate) { if (startDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate); void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
} }
@@ -64,13 +111,21 @@ export const auditLogDALFactory = (db: TDbClient) => {
} }
const docs = await sqlQuery; const docs = await sqlQuery;
return docs.map((doc) => ({ return docs.map((doc) => {
...AuditLogsSchema.parse(doc), // Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
project: { // This is a quick and dirty way to get around the types.
name: doc.projectName, const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
slug: doc.projectSlug
} return {
})); ...AuditLogsSchema.parse(doc),
...(projectDoc?.projectSlug && {
project: {
name: projectDoc.projectName,
slug: projectDoc.projectSlug
}
})
};
});
} catch (error) { } catch (error) {
throw new DatabaseError({ error }); throw new DatabaseError({ error });
} }
@@ -23,30 +23,19 @@ export const auditLogServiceFactory = ({
auditLogQueue, auditLogQueue,
permissionService permissionService
}: TAuditLogServiceFactoryDep) => { }: TAuditLogServiceFactoryDep) => {
const listAuditLogs = async ({ const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
userAgentType, // Filter logs for specific project
eventType, if (filter.projectId) {
offset,
limit,
endDate,
startDate,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
auditLogActor
}: TListProjectAuditLogDTO) => {
if (projectId) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, filter.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
} else { } else {
// Organization-wide logs
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -57,22 +46,23 @@ export const auditLogServiceFactory = ({
/** /**
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved * NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
* to the organization level * to the organization level ✅
*/ */
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
} }
// If project ID is not provided, then we need to return all the audit logs for the organization itself. // If project ID is not provided, then we need to return all the audit logs for the organization itself.
const auditLogs = await auditLogDAL.find({ const auditLogs = await auditLogDAL.find({
startDate, startDate: filter.startDate,
endDate, endDate: filter.endDate,
limit, limit: filter.limit,
offset, offset: filter.offset,
eventType, eventType: filter.eventType,
userAgentType, userAgentType: filter.userAgentType,
actor: auditLogActor, actorId: filter.auditLogActorId,
...(projectId ? { projectId } : { orgId: actorOrgId }) actorType: filter.actorType,
eventMetadata: filter.eventMetadata,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
}); });
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({ return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
@@ -5,19 +5,23 @@ import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export type TListProjectAuditLogDTO = { export type TListProjectAuditLogDTO = {
auditLogActor?: string; filter: {
projectId?: string; userAgentType?: UserAgentType;
eventType?: string; eventType?: EventType[];
startDate?: string; offset?: number;
endDate?: string; limit: number;
userAgentType?: string; endDate?: string;
limit?: number; startDate?: string;
offset?: number; projectId?: string;
auditLogActorId?: string;
actorType?: ActorType;
eventMetadata?: Record<string, string>;
};
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateAuditLogDTO = { export type TCreateAuditLogDTO = {
event: Event; event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor; actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
orgId?: string; orgId?: string;
projectId?: string; projectId?: string;
} & BaseAuthData; } & BaseAuthData;
@@ -177,7 +181,8 @@ export enum EventType {
UPDATE_SLACK_INTEGRATION = "update-slack-integration", UPDATE_SLACK_INTEGRATION = "update-slack-integration",
DELETE_SLACK_INTEGRATION = "delete-slack-integration", DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config" UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -198,6 +203,8 @@ interface IdentityActorMetadata {
interface ScimClientActorMetadata {} interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
export interface UserActor { export interface UserActor {
type: ActorType.USER; type: ActorType.USER;
metadata: UserActorMetadata; metadata: UserActorMetadata;
@@ -208,6 +215,11 @@ export interface ServiceActor {
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export interface IdentityActor { export interface IdentityActor {
type: ActorType.IDENTITY; type: ActorType.IDENTITY;
metadata: IdentityActorMetadata; metadata: IdentityActorMetadata;
@@ -218,7 +230,7 @@ export interface ScimClientActor {
metadata: ScimClientActorMetadata; metadata: ScimClientActorMetadata;
} }
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor; export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@@ -1518,6 +1530,16 @@ interface GetProjectSlackConfig {
id: string; id: string;
}; };
} }
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
@@ -1657,4 +1679,5 @@ export type Event =
| DeleteSlackIntegration | DeleteSlackIntegration
| GetSlackIntegration | GetSlackIntegration
| UpdateProjectSlackConfig | UpdateProjectSlackConfig
| GetProjectSlackConfig; | GetProjectSlackConfig
| IntegrationSyncedEvent;
@@ -1,10 +1,70 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>; export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
export const dynamicSecretDALFactory = (db: TDbClient) => { export const dynamicSecretDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecret); const orm = ormify(db, TableName.DynamicSecret);
return orm;
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
const listDynamicSecretsByFolderIds = async (
{
folderIds,
search,
limit,
offset = 0,
orderBy = SecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC
}: {
folderIds: string[];
search?: string;
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
},
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.whereIn("folderId", folderIds)
.where((bd) => {
if (search) {
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
}
})
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select(
selectAllTableCols(TableName.DynamicSecret),
db.ref("slug").withSchema(TableName.Environment).as("environment"),
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
)
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
if (limit) {
const rankOffset = offset + 1;
return await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", rankOffset)
.andWhere("w.rank", "<", rankOffset + limit);
}
const dynamicSecrets = await query;
return dynamicSecrets;
} catch (error) {
throw new DatabaseError({ error, name: "List dynamic secret multi env" });
}
};
return { ...orm, listDynamicSecretsByFolderIds };
}; };
@@ -6,6 +6,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -17,9 +18,12 @@ import {
TCreateDynamicSecretDTO, TCreateDynamicSecretDTO,
TDeleteDynamicSecretDTO, TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO, TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO,
TListDynamicSecretsDTO, TListDynamicSecretsDTO,
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 = {
@@ -31,7 +35,7 @@ type TDynamicSecretServiceFactoryDep = {
"pruneDynamicSecret" | "unsetLeaseRevocation" "pruneDynamicSecret" | "unsetLeaseRevocation"
>; >;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
}; };
@@ -300,19 +304,55 @@ export const dynamicSecretServiceFactory = ({
return { ...dynamicSecretCfg, inputs: providerInputs }; return { ...dynamicSecretCfg, inputs: providerInputs };
}; };
const list = async ({ // get unique dynamic secret count across multiple envs
const getCountMultiEnv = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
actor, actor,
projectSlug, projectId,
path, path,
environmentSlug environmentSlugs,
}: TListDynamicSecretsDTO) => { search
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); }: TListDynamicSecretsMultiEnvDTO) => {
if (!project) throw new BadRequestError({ message: "Project not found" }); const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
const projectId = project.id; // verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
)
);
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find(
{ $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined },
{ countDistinct: "name" }
);
return Number(dynamicSecretCfg[0]?.count ?? 0);
};
// get dynamic secret count for a single env
const getDynamicSecretCount = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
path,
environmentSlug,
search,
projectId
}: TGetDynamicSecretsCountDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -328,15 +368,127 @@ export const dynamicSecretServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.find(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
{ count: true }
);
return Number(dynamicSecretCfg[0]?.count ?? 0);
};
const listDynamicSecretsByEnv = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
projectSlug,
path,
environmentSlug,
limit,
offset,
orderBy,
orderDirection = OrderByDirection.ASC,
search,
...params
}: TListDynamicSecretsDTO) => {
let { projectId } = params;
if (!projectId) {
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
projectId = project.id;
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
{
limit,
offset,
sort: orderBy ? [[orderBy, orderDirection]] : undefined
}
);
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
// get dynamic secrets for multiple envs
const listDynamicSecretsByFolderIds = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
path,
environmentSlugs,
projectId,
...params
}: TListDynamicSecretsMultiEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
)
);
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
folderIds: folders.map((folder) => folder.id),
...params
});
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,
deleteByName, deleteByName,
getDetails, getDetails,
list listDynamicSecretsByEnv,
listDynamicSecretsByFolderIds,
getDynamicSecretCount,
getCountMultiEnv,
fetchAzureEntraIdUsers
}; };
}; };
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { DynamicSecretProviderSchema } from "./providers/models"; import { DynamicSecretProviderSchema } from "./providers/models";
@@ -50,5 +51,20 @@ export type TDetailsDynamicSecretDTO = {
export type TListDynamicSecretsDTO = { export type TListDynamicSecretsDTO = {
path: string; path: string;
environmentSlug: string; environmentSlug: string;
projectSlug: string; projectSlug?: string;
projectId?: string;
offset?: number;
limit?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsMultiEnvDTO = Omit<
TListDynamicSecretsDTO,
"projectId" | "environmentSlug" | "projectSlug"
> & { projectId: string; environmentSlugs: string[] };
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
projectId: string;
};
@@ -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",
@@ -27,7 +25,8 @@ export enum OrgPermissionSubjects {
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity", Identity = "identity",
Kms = "kms", Kms = "kms",
AdminConsole = "organization-admin-console" AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@@ -45,10 +44,11 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [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);
@@ -113,15 +113,20 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
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 +147,16 @@ 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 }); can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
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,26 +47,29 @@ export const permissionServiceFactory = ({
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL
}: TPermissionServiceFactoryDep) => { }: TPermissionServiceFactoryDep) => {
const buildOrgPermission = (role: string, permission?: unknown) => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
switch (role) { const rules = orgUserRoles
case OrgMembershipRole.Admin: .map(({ role, permissions }) => {
return orgAdminPermissions; switch (role) {
case OrgMembershipRole.Member: case OrgMembershipRole.Admin:
return orgMemberPermissions; return orgAdminPermissions;
case OrgMembershipRole.NoAccess: case OrgMembershipRole.Member:
return orgNoAccessPermissions; return orgMemberPermissions;
case OrgMembershipRole.Custom: case OrgMembershipRole.NoAccess:
return createMongoAbility<OrgPermissionSet>( return orgNoAccessPermissions;
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>( case OrgMembershipRole.Custom:
permission as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] return unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
), permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
{ );
conditionsMatcher default:
} throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
); }
default: })
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;
}[];
@@ -145,6 +145,8 @@ export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermiss
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags], [ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags], [ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions.
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs], [ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs], [ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs], [ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
+36 -1
View File
@@ -698,11 +698,46 @@ export const SECRET_IMPORTS = {
} }
} as const; } as const;
export const DASHBOARD = {
SECRET_OVERVIEW_LIST: {
projectId: "The ID of the project to list secrets/folders from.",
environments:
"The slugs of the environments to list secrets/folders from (comma separated, ie 'environments=dev,staging,prod').",
secretPath: "The secret path to list secrets/folders from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
limit: "The number of secrets/folders to return.",
orderBy: "The column to order secrets/folders by.",
orderDirection: "The direction to order secrets/folders in.",
search: "The text string to filter secret keys and folder names by.",
includeSecrets: "Whether to include project secrets in the response.",
includeFolders: "Whether to include project folders in the response.",
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
},
SECRET_DETAILS_LIST: {
projectId: "The ID of the project to list secrets/folders from.",
environment: "The slug of the environment to list secrets/folders from.",
secretPath: "The secret path to list secrets/folders from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
limit: "The number of secrets/folders to return.",
orderBy: "The column to order secrets/folders by.",
orderDirection: "The direction to order secrets/folders in.",
search: "The text string to filter secret keys and folder names by.",
tags: "The tags to filter secrets by (comma separated, ie 'tags=billing,engineering').",
includeSecrets: "Whether to include project secrets in the response.",
includeFolders: "Whether to include project folders in the response.",
includeImports: "Whether to include project secret imports in the response.",
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
}
} as const;
export const AUDIT_LOGS = { export const AUDIT_LOGS = {
EXPORT: { EXPORT: {
workspaceId: "The ID of the project to export audit logs from.", projectId:
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
eventType: "The type of the event to export.", eventType: "The type of the event to export.",
userAgentType: "Choose which consuming application to export audit logs for.", userAgentType: "Choose which consuming application to export audit logs for.",
eventMetadata:
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
startDate: "The date to start the export from.", startDate: "The date to start the export from.",
endDate: "The date to end the export at.", endDate: "The date to end the export at.",
offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.", offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
+2 -2
View File
@@ -147,8 +147,8 @@ const envSchema = z
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()), PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string()).optional(), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string()).optional() WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
}) })
.transform((data) => ({ .transform((data) => ({
...data, ...data,
+17 -5
View File
@@ -51,11 +51,17 @@ export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean
: unknown) : unknown)
>; >;
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = { export type TFindOpt<
R extends object = object,
TCount extends boolean = boolean,
TCountDistinct extends keyof R | undefined = undefined
> = {
limit?: number; limit?: number;
offset?: number; offset?: number;
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>; sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
groupBy?: keyof R;
count?: TCount; count?: TCount;
countDistinct?: TCountDistinct;
tx?: Knex; tx?: Knex;
}; };
@@ -86,13 +92,18 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Find one" }); throw new DatabaseError({ error, name: "Find one" });
} }
}, },
find: async <TCount extends boolean = false>( find: async <
TCount extends boolean = false,
TCountDistinct extends keyof Tables[Tname]["base"] | undefined = undefined
>(
filter: TFindFilter<Tables[Tname]["base"]>, filter: TFindFilter<Tables[Tname]["base"]>,
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {} { offset, limit, sort, count, tx, countDistinct }: TFindOpt<Tables[Tname]["base"], TCount, TCountDistinct> = {}
) => { ) => {
try { try {
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter)); const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
if (count) { if (countDistinct) {
void query.countDistinct(countDistinct);
} else if (count) {
void query.select(db.raw("COUNT(*) OVER() AS count")); void query.select(db.raw("COUNT(*) OVER() AS count"));
void query.select("*"); void query.select("*");
} }
@@ -101,7 +112,8 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
if (sort) { if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
} }
const res = (await query) as TFindReturn<typeof query, TCount>;
const res = (await query) as TFindReturn<typeof query, TCountDistinct extends undefined ? TCount : true>;
return res; return res;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find one" }); throw new DatabaseError({ error, name: "Find one" });
+26 -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,16 +93,26 @@ 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: { | {
projectId: string; name: QueueJobs.IdentityAccessTokenStatusUpdate;
environment: string; payload: { identityAccessTokenId: string; numberOfUses: number };
secretPath: string; }
depth?: number; | {
deDupeQueue?: Record<string, boolean>; name: QueueJobs.ServiceTokenStatusUpdate;
}; payload: { serviceTokenId: string };
}; };
[QueueName.IntegrationSync]:
| {
name: QueueJobs.IntegrationSync;
payload: TIntegrationSyncPayload;
}
| {
name: QueueJobs.SendFailedIntegrationSyncEmails;
payload: TFailedIntegrationSyncEmailsPayload;
};
[QueueName.SecretFullRepoScan]: { [QueueName.SecretFullRepoScan]: {
name: QueueJobs.SecretScan; name: QueueJobs.SecretScan;
payload: TScanFullRepoEventPayload; payload: TScanFullRepoEventPayload;
@@ -151,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);
} }
+2 -1
View File
@@ -493,7 +493,6 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
userGroupMembershipDAL,
projectBotDAL, projectBotDAL,
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
@@ -811,6 +810,8 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
webhookDAL, webhookDAL,
orgDAL, orgDAL,
auditLogService,
userDAL,
projectMembershipDAL, projectMembershipDAL,
smtpService, smtpService,
projectDAL, projectDAL,
@@ -4,7 +4,7 @@ import { IntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { INTEGRATION } from "@app/lib/api-docs"; import { INTEGRATION } from "@app/lib/api-docs";
import { removeTrailingSlash, shake } from "@app/lib/fn"; import { removeTrailingSlash, shake } from "@app/lib/fn";
import { writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -154,6 +154,48 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/:integrationId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get an integration by integration id",
security: [
{
bearerAuth: []
}
],
params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
}),
response: {
200: z.object({
integration: IntegrationsSchema.extend({
environment: z.object({
slug: z.string().trim(),
name: z.string().trim(),
id: z.string().trim()
})
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integration = await server.services.integration.getIntegration({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
});
return { integration };
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:integrationId", url: "/:integrationId",
@@ -14,7 +14,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn"; import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
export const registerOrgRouter = async (server: FastifyZodProvider) => { export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -74,8 +74,36 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Get all audit logs for an organization", description: "Get all audit logs for an organization",
querystring: z.object({ querystring: z.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
actorType: z.nativeEnum(ActorType).optional(),
// eventType is split with , for multiple values, we need to transform it to array
eventType: z
.string()
.optional()
.transform((val) => (val ? val.split(",") : undefined)),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType), userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
eventMetadata: z
.string()
.optional()
.transform((val) => {
if (!val) {
return undefined;
}
const pairs = val.split(",");
return pairs.reduce(
(acc, pair) => {
const [key, value] = pair.split("=");
if (key && value) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
})
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
@@ -93,10 +121,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}) })
.merge( .merge(
z.object({ z.object({
project: z.object({ project: z
name: z.string(), .object({
slug: z.string() name: z.string(),
}), slug: z.string()
})
.optional(),
event: z.object({ event: z.object({
type: z.string(), type: z.string(),
metadata: z.any() metadata: z.any()
@@ -114,13 +144,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const auditLogs = await server.services.auditLog.listAuditLogs({ const auditLogs = await server.services.auditLog.listAuditLogs({
filter: {
...req.query,
endDate: req.query.endDate,
projectId: req.query.projectId,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
actorType: req.query.actorType,
eventType: req.query.eventType as EventType[] | undefined
},
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
...req.query,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type actor: req.permission.type
}); });
return { auditLogs }; return { auditLogs };
@@ -0,0 +1,612 @@
import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types";
import { secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/secrets-overview",
config: {
rateLimit: secretsLimit
},
schema: {
description: "List project secrets overview",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.projectId),
environments: z
.string()
.trim()
.transform(decodeURIComponent)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.environments),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.secretPath),
offset: z.coerce.number().min(0).optional().default(0).describe(DASHBOARD.SECRET_OVERVIEW_LIST.offset),
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(DASHBOARD.SECRET_OVERVIEW_LIST.limit),
orderBy: z
.nativeEnum(SecretsOrderBy)
.default(SecretsOrderBy.Name)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderBy)
.optional(),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
.optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
includeSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
includeFolders: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
includeDynamicSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(),
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
.array()
.optional(),
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const {
secretPath,
projectId,
limit,
offset,
search,
orderBy,
orderDirection,
includeFolders,
includeSecrets,
includeDynamicSecrets
} = req.query;
const environments = req.query.environments.split(",");
if (!projectId || environments.length === 0)
throw new BadRequestError({ message: "Missing workspace id or environment(s)" });
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
// prevent older projects from accessing endpoint
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
let remainingLimit = limit;
let adjustedOffset = offset;
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByFolderIds>>
| undefined;
let totalFolderCount: number | undefined;
let totalDynamicSecretCount: number | undefined;
let totalSecretCount: number | undefined;
if (includeFolders) {
// this is the unique count, ie duplicate folders across envs only count as 1
totalFolderCount = await server.services.folder.getProjectFolderCount({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.query.projectId,
path: secretPath,
environments,
search
});
if (remainingLimit > 0 && totalFolderCount > adjustedOffset) {
folders = await server.services.folder.getFoldersMultiEnv({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
environments,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset
});
// get the count of unique folder names to properly adjust remaining limit
const uniqueFolderCount = new Set(folders.map((folder) => folder.name)).size;
remainingLimit -= uniqueFolderCount;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalFolderCount);
}
}
if (includeDynamicSecrets) {
// this is the unique count, ie duplicate secrets across envs only count as 1
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
search,
environmentSlugs: environments,
path: secretPath
});
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
search,
orderBy,
orderDirection,
environmentSlugs: environments,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset
});
// get the count of unique dynamic secret names to properly adjust remaining limit
const uniqueDynamicSecretsCount = new Set(dynamicSecrets.map((dynamicSecret) => dynamicSecret.name)).size;
remainingLimit -= uniqueDynamicSecretsCount;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
}
}
if (includeSecrets) {
// this is the unique count, ie duplicate secrets across envs only count as 1
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
search
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = await server.services.secret.getSecretsRawMultiEnv({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset
});
for await (const environment of environments) {
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountFromEnv) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountFromEnv
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secretCountFromEnv,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
}
}
}
return {
folders,
dynamicSecrets,
secrets,
totalFolderCount,
totalDynamicSecretCount,
totalSecretCount,
totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
};
}
});
server.route({
method: "GET",
url: "/secrets-details",
config: {
rateLimit: secretsLimit
},
schema: {
description: "List project secrets details",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.projectId),
environment: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(DASHBOARD.SECRET_DETAILS_LIST.secretPath),
offset: z.coerce.number().min(0).optional().default(0).describe(DASHBOARD.SECRET_DETAILS_LIST.offset),
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(DASHBOARD.SECRET_DETAILS_LIST.limit),
orderBy: z
.nativeEnum(SecretsOrderBy)
.default(SecretsOrderBy.Name)
.describe(DASHBOARD.SECRET_DETAILS_LIST.orderBy)
.optional(),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(DASHBOARD.SECRET_DETAILS_LIST.orderDirection)
.optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
includeSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
includeImports: z.coerce
.boolean()
.optional()
.default(true)
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
}),
response: {
200: z.object({
imports: SecretImportsSchema.omit({ importEnv: true })
.extend({
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
})
.array()
.optional(),
folders: SecretFoldersSchema.array().optional(),
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
.array()
.optional(),
totalImportCount: z.number().optional(),
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const {
secretPath,
environment,
projectId,
limit,
offset,
search,
orderBy,
orderDirection,
includeFolders,
includeSecrets,
includeDynamicSecrets,
includeImports
} = req.query;
if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
// prevent older projects from accessing endpoint
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
const tags = req.query.tags?.split(",") ?? [];
let remainingLimit = limit;
let adjustedOffset = offset;
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"] | undefined;
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let totalImportCount: number | undefined;
let totalFolderCount: number | undefined;
let totalDynamicSecretCount: number | undefined;
let totalSecretCount: number | undefined;
if (includeImports) {
totalImportCount = await server.services.secretImport.getProjectImportCount({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
environment,
path: secretPath,
search
});
if (remainingLimit > 0 && totalImportCount > adjustedOffset) {
imports = await server.services.secretImport.getImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
environment,
path: secretPath,
search,
limit: remainingLimit,
offset: adjustedOffset
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment,
folderId: imports?.[0]?.folderId,
numberOfImports: imports.length
}
}
});
remainingLimit -= imports.length;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalImportCount);
}
}
if (includeFolders) {
totalFolderCount = await server.services.folder.getProjectFolderCount({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
path: secretPath,
environments: [environment],
search
});
if (remainingLimit > 0 && totalFolderCount > adjustedOffset) {
folders = await server.services.folder.getFolders({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
environment,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset
});
remainingLimit -= folders.length;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalFolderCount);
}
}
if (includeDynamicSecrets) {
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
search,
environmentSlug: environment,
path: secretPath
});
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnv({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId,
search,
orderBy,
orderDirection,
environmentSlug: environment,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset
});
remainingLimit -= dynamicSecrets.length;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
}
}
if (includeSecrets) {
totalSecretCount = await server.services.secret.getSecretsCount({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
search,
tagSlugs: tags
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
const secretsRaw = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset,
tagSlugs: tags
});
secrets = secretsRaw.secrets;
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
}
return {
imports,
folders,
dynamicSecrets,
secrets,
totalImportCount,
totalFolderCount,
totalDynamicSecretCount,
totalSecretCount,
totalCount:
(totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
};
}
});
};
+2
View File
@@ -1,3 +1,4 @@
import { registerDashboardRouter } from "./dashboard-router";
import { registerGroupProjectRouter } from "./group-project-router"; import { registerGroupProjectRouter } from "./group-project-router";
import { registerLoginRouter } from "./login-router"; import { registerLoginRouter } from "./login-router";
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router"; import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
@@ -17,4 +18,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
}, },
{ prefix: "/workspaces" } { prefix: "/workspaces" }
); );
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
}; };
+1
View File
@@ -34,6 +34,7 @@ export enum AuthMode {
} }
export enum ActorType { // would extend to AWS, Azure, ... export enum ActorType { // would extend to AWS, Azure, ...
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
USER = "user", // userIdentity USER = "user", // userIdentity
SERVICE = "service", SERVICE = "service",
IDENTITY = "identity", IDENTITY = "identity",
@@ -157,7 +157,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
`${TableName.ProjectRoles}.id` `${TableName.ProjectRoles}.id`
) )
.select( .select(
db.ref("id").withSchema(TableName.GroupProjectMembership), db.ref("id").withSchema(TableName.UserGroupMembership),
db.ref("isGhost").withSchema(TableName.Users), db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
@@ -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>;
issuer: identityOidcAuth.boundIssuer try {
}) as Record<string, string>; tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer
}) 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."
}); });
} }
@@ -58,7 +58,8 @@ export const identityServiceFactory = ({
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" }); if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" });
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities." message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities."
@@ -242,37 +242,12 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
}; };
} }
const octokit = new Octokit({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const repos = (await new Octokit({
auth: accessToken auth: accessToken
}); }).paginate("GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", {
per_page: 100
const getAllRepos = async () => { })) as GitHubApp[];
let repos: GitHubApp[] = [];
let page = 1;
const perPage = 100;
let hasMore = true;
while (hasMore) {
const response = await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page: perPage,
page
}
);
if ((response.data as GitHubApp[]).length > 0) {
repos = repos.concat(response.data as GitHubApp[]);
page += 1;
} else {
hasMore = false;
}
}
return repos;
};
const repos = await getAllRepos();
const apps = repos const apps = repos
.filter((a: GitHubApp) => a.permissions.admin === true) .filter((a: GitHubApp) => a.permissions.admin === true)
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
@@ -19,6 +19,7 @@ import { TIntegrationDALFactory } from "./integration-dal";
import { import {
TCreateIntegrationDTO, TCreateIntegrationDTO,
TDeleteIntegrationDTO, TDeleteIntegrationDTO,
TGetIntegrationDTO,
TSyncIntegrationDTO, TSyncIntegrationDTO,
TUpdateIntegrationDTO TUpdateIntegrationDTO
} from "./integration-types"; } from "./integration-types";
@@ -180,6 +181,27 @@ export const integrationServiceFactory = ({
return updatedIntegration; return updatedIntegration;
}; };
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integration?.projectId || "",
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
if (!integration) {
throw new NotFoundError({
message: "Integration not found"
});
}
return { ...integration, envId: integration.environment.id };
};
const deleteIntegration = async ({ const deleteIntegration = async ({
actorId, actorId,
id, id,
@@ -276,6 +298,8 @@ export const integrationServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
await secretQueueService.syncIntegrations({ await secretQueueService.syncIntegrations({
isManual: true,
actorId,
environment: integration.environment.slug, environment: integration.environment.slug,
secretPath: integration.secretPath, secretPath: integration.secretPath,
projectId: integration.projectId projectId: integration.projectId
@@ -289,6 +313,7 @@ export const integrationServiceFactory = ({
updateIntegration, updateIntegration,
deleteIntegration, deleteIntegration,
listIntegrationByProject, listIntegrationByProject,
getIntegration,
syncIntegration syncIntegration
}; };
}; };
@@ -39,6 +39,10 @@ export type TCreateIntegrationDTO = {
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetIntegrationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateIntegrationDTO = { export type TUpdateIntegrationDTO = {
id: string; id: string;
app?: string; app?: string;
+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()
}, },
+24 -29
View File
@@ -17,7 +17,6 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects"; import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -90,7 +89,6 @@ type TOrgServiceFactoryDep = {
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
}; };
@@ -116,7 +114,6 @@ export const orgServiceFactory = ({
licenseService, licenseService,
projectRoleDAL, projectRoleDAL,
samlConfigDAL, samlConfigDAL,
userGroupMembershipDAL,
projectBotDAL, projectBotDAL,
projectUserMembershipRoleDAL projectUserMembershipRoleDAL
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
@@ -461,12 +458,6 @@ export const orgServiceFactory = ({
const org = await orgDAL.findOrgById(orgId); const org = await orgDAL.findOrgById(orgId);
if (org?.authEnforced) {
throw new BadRequestError({
message: "Failed to invite user due to org-level auth enforced for organization"
});
}
const isEmailInvalid = await isDisposableEmail(inviteeEmails); const isEmailInvalid = await isDisposableEmail(inviteeEmails);
if (isEmailInvalid) { if (isEmailInvalid) {
throw new BadRequestError({ throw new BadRequestError({
@@ -475,19 +466,6 @@ export const orgServiceFactory = ({
}); });
} }
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole); const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
if (isCustomOrgRole) { if (isCustomOrgRole) {
if (!plan?.rbac) if (!plan?.rbac)
@@ -572,7 +550,7 @@ export const orgServiceFactory = ({
); );
} }
const [inviteeMembership] = await orgDAL.findMembership( const [inviteeOrgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId [`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId
@@ -581,7 +559,27 @@ export const orgServiceFactory = ({
); );
// if there exist no org membership we set is as given by the request // if there exist no org membership we set is as given by the request
if (!inviteeMembership) { if (!inviteeOrgMembership) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
if (org?.authEnforced) {
throw new BadRequestError({
message: "Failed to invite user due to org-level auth enforced for organization"
});
}
// as its used by project invite also // as its used by project invite also
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
let roleId; let roleId;
@@ -617,7 +615,6 @@ export const orgServiceFactory = ({
} }
const userIds = users.map(({ id }) => id); const userIds = users.map(({ id }) => id);
const usernames = users.map((el) => el.username);
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx); const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
// we don't need to spam with email. Thus org invitation doesn't need project invitation again // we don't need to spam with email. Thus org invitation doesn't need project invitation again
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId)); const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
@@ -644,12 +641,10 @@ export const orgServiceFactory = ({
{ tx } { tx }
); );
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId); const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId);
const userIdsToExcludeAsPartOfGroup = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernames, projectId, tx)
);
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter( const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
(user) => !existingMembersGroupByUserId?.[user.userId] && !userIdsToExcludeAsPartOfGroup.has(user.userId) (user) => !existingMembersGroupByUserId?.[user.userId]
); );
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
if (!userWithEncryptionKeyInvitedToProject.length) continue; if (!userWithEncryptionKeyInvitedToProject.length) continue;
@@ -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 };
@@ -90,15 +90,20 @@ export const projectMembershipServiceFactory = ({
// projectMembers[0].project // projectMembers[0].project
if (includeGroupMembers) { if (includeGroupMembers) {
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId); const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
const allMembers = [ const allMembers = [
...projectMembers.map((m) => ({ ...m, isGroupMember: false })), ...projectMembers.map((m) => ({ ...m, isGroupMember: false })),
...groupMembers.map((m) => ({ ...m, isGroupMember: true })) ...groupMembers.map((m) => ({ ...m, isGroupMember: true }))
]; ];
// Ensure the userId is unique // Ensure the userId is unique
const membersIds = new Set(allMembers.map((entity) => entity.user.id)); const uniqueMembers: typeof allMembers = [];
const uniqueMembers = allMembers.filter((entity) => membersIds.has(entity.user.id)); const addedUserIds = new Set<string>();
allMembers.forEach((member) => {
if (!addedUserIds.has(member.user.id)) {
uniqueMembers.push(member);
addedUserIds.add(member.user.id);
}
});
return uniqueMembers; return uniqueMembers;
} }
@@ -5,6 +5,8 @@ import { TableName, TProjectEnvironments, TSecretFolders, TSecretFoldersUpdate }
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { groupBy, removeTrailingSlash } from "@app/lib/fn"; import { groupBy, removeTrailingSlash } from "@app/lib/fn";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
export const validateFolderName = (folderName: string) => { export const validateFolderName = (folderName: string) => {
const validNameRegex = /^[a-zA-Z0-9-_]+$/; const validNameRegex = /^[a-zA-Z0-9-_]+$/;
@@ -83,7 +85,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
.from<TSecretFolders & { depth: number; path: string }>("parent"); .from<TSecretFolders & { depth: number; path: string }>("parent");
}; };
const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: string, secretPath: string) => { const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: string[], secretPath: string) => {
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2 // this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
const formatedPath = secretPath.at(-1) === "/" && secretPath.length > 1 ? secretPath.slice(0, -1) : secretPath; const formatedPath = secretPath.at(-1) === "/" && secretPath.length > 1 ? secretPath.slice(0, -1) : secretPath;
// next goal to sanitize saw the raw sql query is safe // next goal to sanitize saw the raw sql query is safe
@@ -111,7 +113,7 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: stri
projectId, projectId,
parentId: null parentId: null
}) })
.where(`${TableName.Environment}.slug`, environment) .whereIn(`${TableName.Environment}.slug`, environments)
.select(selectAllTableCols(TableName.SecretFolder)) .select(selectAllTableCols(TableName.SecretFolder))
.union( .union(
(qb) => (qb) =>
@@ -139,14 +141,14 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: stri
.from<TSecretFolders & { depth: number; path: string }>("parent") .from<TSecretFolders & { depth: number; path: string }>("parent")
.leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId") .leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId")
.select< .select<
TSecretFolders & { (TSecretFolders & {
depth: number; depth: number;
path: string; path: string;
envId: string; envId: string;
envSlug: string; envSlug: string;
envName: string; envName: string;
projectId: string; projectId: string;
} })[]
>( >(
selectAllTableCols("parent" as TableName.SecretFolder), selectAllTableCols("parent" as TableName.SecretFolder),
db.ref("id").withSchema(TableName.Environment).as("envId"), db.ref("id").withSchema(TableName.Environment).as("envId"),
@@ -214,7 +216,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const folder = await sqlFindFolderByPathQuery( const folder = await sqlFindFolderByPathQuery(
tx || db.replicaNode(), tx || db.replicaNode(),
projectId, projectId,
environment, [environment],
removeTrailingSlash(path) removeTrailingSlash(path)
) )
.orderBy("depth", "desc") .orderBy("depth", "desc")
@@ -230,6 +232,35 @@ export const secretFolderDALFactory = (db: TDbClient) => {
} }
}; };
// finds folders by path for multiple envs
const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => {
try {
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1;
const folders = await sqlFindFolderByPathQuery(
tx || db.replicaNode(),
projectId,
environments,
removeTrailingSlash(path)
)
.orderBy("depth", "desc")
.where("depth", pathDepth);
const firstFolder = folders[0];
if (firstFolder && firstFolder.path !== removeTrailingSlash(path)) {
return [];
}
return folders.map((folder) => {
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
return { ...el, envId: id, environment: { id, name, slug } };
});
} catch (error) {
throw new DatabaseError({ error, name: "Find folders by secret path multi env" });
}
};
// used in folder creation // used in folder creation
// even if its the original given /path1/path2 // even if its the original given /path1/path2
// it will stop automatically at /path2 // it will stop automatically at /path2
@@ -238,7 +269,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const folder = await sqlFindFolderByPathQuery( const folder = await sqlFindFolderByPathQuery(
tx || db.replicaNode(), tx || db.replicaNode(),
projectId, projectId,
environment, [environment],
removeTrailingSlash(path) removeTrailingSlash(path)
) )
.orderBy("depth", "desc") .orderBy("depth", "desc")
@@ -352,14 +383,77 @@ export const secretFolderDALFactory = (db: TDbClient) => {
} }
}; };
// find project folders for multiple envs
const findByMultiEnv = async (
{
environmentIds,
parentIds,
search,
limit,
offset = 0,
orderBy = SecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC
}: {
environmentIds: string[];
parentIds: string[];
search?: string;
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
},
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretFolder)
.whereIn("parentId", parentIds)
.whereIn("envId", environmentIds)
.where("isReserved", false)
.where((bd) => {
if (search) {
void bd.whereILike(`${TableName.SecretFolder}.name`, `%${search}%`);
}
})
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw(
`DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" ${
orderDirection ?? OrderByDirection.ASC
}) as rank`
),
db.ref("slug").withSchema(TableName.Environment).as("environment")
)
.orderBy(`${TableName.SecretFolder}.${orderBy}`, orderDirection);
if (limit) {
const rankOffset = offset + 1; // ranks start from 1
return await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", rankOffset)
.andWhere("w.rank", "<", rankOffset + limit);
}
const folders = await query;
return folders;
} catch (error) {
throw new DatabaseError({ error, name: "Find folders multi env" });
}
};
return { return {
...secretFolderOrm, ...secretFolderOrm,
update, update,
findBySecretPath, findBySecretPath,
findBySecretPathMultiEnv,
findById, findById,
findByManySecretPath, findByManySecretPath,
findSecretPathByFolderIds, findSecretPathByFolderIds,
findClosestFolder, findClosestFolder,
findByProjectId findByProjectId,
findByMultiEnv
}; };
}; };
@@ -7,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -26,7 +27,7 @@ type TSecretFolderServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
folderDAL: TSecretFolderDALFactory; folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
folderVersionDAL: TSecretFolderVersionDALFactory; folderVersionDAL: TSecretFolderVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
}; };
@@ -396,7 +397,12 @@ export const secretFolderServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
environment, environment,
path: secretPath path: secretPath,
search,
orderBy,
orderDirection,
limit,
offset
}: TGetFolderDTO) => { }: TGetFolderDTO) => {
// folder list is allowed to be read by anyone // folder list is allowed to be read by anyone
// permission to check does user has access // permission to check does user has access
@@ -408,11 +414,92 @@ export const secretFolderServiceFactory = ({
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!parentFolder) return []; if (!parentFolder) return [];
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id, isReserved: false }); const folders = await folderDAL.find(
{
envId: env.id,
parentId: parentFolder.id,
isReserved: false,
$search: search ? { name: `%${search}%` } : undefined
},
{
sort: orderBy ? [[orderBy, orderDirection ?? OrderByDirection.ASC]] : undefined,
limit,
offset
}
);
return folders;
};
// get folders for multiple envs
const getFoldersMultiEnv = async ({
projectId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environments,
path: secretPath,
...params
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
// folder list is allowed to be read by anyone
// permission to check does user has access
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
if (!envs.length)
throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" });
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
if (!parentFolders.length) return [];
const folders = await folderDAL.findByMultiEnv({
environmentIds: envs.map((env) => env.id),
parentIds: parentFolders.map((folder) => folder.id),
...params
});
return folders; return folders;
}; };
// get the unique count of folders within a project path
const getProjectFolderCount = async ({
projectId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environments,
path: secretPath,
search
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
// folder list is allowed to be read by anyone
// permission to check does user has access
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
if (!envs.length)
throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" });
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
if (!parentFolders.length) return 0;
const folders = await folderDAL.find(
{
$in: {
envId: envs.map((env) => env.id),
parentId: parentFolders.map((folder) => folder.id)
},
isReserved: false,
$search: search ? { name: `%${search}%` } : undefined
},
{ countDistinct: "name" }
);
return Number(folders[0]?.count ?? 0);
};
const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => { const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => {
const folder = await folderDAL.findById(id); const folder = await folderDAL.findById(id);
if (!folder) throw new NotFoundError({ message: "folder not found" }); if (!folder) throw new NotFoundError({ message: "folder not found" });
@@ -429,6 +516,8 @@ export const secretFolderServiceFactory = ({
updateManyFolders, updateManyFolders,
deleteFolder, deleteFolder,
getFolders, getFolders,
getFolderById getFolderById,
getProjectFolderCount,
getFoldersMultiEnv
}; };
}; };
@@ -1,4 +1,5 @@
import { TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
export enum ReservedFolders { export enum ReservedFolders {
SecretReplication = "__reserve_replication_" SecretReplication = "__reserve_replication_"
@@ -36,6 +37,11 @@ export type TDeleteFolderDTO = {
export type TGetFolderDTO = { export type TGetFolderDTO = {
environment: string; environment: string;
path: string; path: string;
search?: string;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
limit?: number;
offset?: number;
} & TProjectPermission; } & TProjectPermission;
export type TGetFolderByIdDTO = { export type TGetFolderByIdDTO = {
@@ -49,10 +49,30 @@ export const secretImportDALFactory = (db: TDbClient) => {
} }
}; };
const find = async (filter: Partial<TSecretImports & { projectId: string }>, tx?: Knex) => { const find = async (
{
search,
limit,
offset,
...filter
}: Partial<
TSecretImports & {
projectId: string;
search?: string;
limit?: number;
offset?: number;
}
>,
tx?: Knex
) => {
try { try {
const docs = await (tx || db.replicaNode())(TableName.SecretImport) const query = (tx || db.replicaNode())(TableName.SecretImport)
.where(filter) .where(filter)
.where((bd) => {
if (search) {
void bd.whereILike("importPath", `%${search}%`);
}
})
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
.select( .select(
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports, db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
@@ -61,6 +81,13 @@ export const secretImportDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.Environment).as("envId") db.ref("id").withSchema(TableName.Environment).as("envId")
) )
.orderBy("position", "asc"); .orderBy("position", "asc");
if (limit) {
void query.limit(limit).offset(offset ?? 0);
}
const docs = await query;
return docs.map(({ envId, slug, name, ...el }) => ({ return docs.map(({ envId, slug, name, ...el }) => ({
...el, ...el,
importEnv: { id: envId, slug, name } importEnv: { id: envId, slug, name }
@@ -70,6 +97,28 @@ export const secretImportDALFactory = (db: TDbClient) => {
} }
}; };
const getProjectImportCount = async (
{ search, ...filter }: Partial<TSecretImports & { projectId: string; search?: string }>,
tx?: Knex
) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
.where(filter)
.where("isReplication", false)
.where((bd) => {
if (search) {
void bd.whereILike("importPath", `%${search}%`);
}
})
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
.count();
return Number(docs[0]?.count ?? 0);
} catch (error) {
throw new DatabaseError({ error, name: "get secret imports count" });
}
};
const findByFolderIds = async (folderIds: string[], tx?: Knex) => { const findByFolderIds = async (folderIds: string[], tx?: Knex) => {
try { try {
const docs = await (tx || db.replicaNode())(TableName.SecretImport) const docs = await (tx || db.replicaNode())(TableName.SecretImport)
@@ -97,6 +146,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
find, find,
findByFolderIds, findByFolderIds,
findLastImportPosition, findLastImportPosition,
updateAllPosition updateAllPosition,
getProjectImportCount
}; };
}; };
@@ -220,7 +220,7 @@ export const fnSecretsV2FromImports = async ({
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId); const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => { const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0]; const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0];
const folderDeeperImportSecrets = const folderDeeperImportSecrets =
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || []; secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []) const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
@@ -7,7 +7,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types"; import { KmsDataKey } from "../kms/kms-types";
@@ -394,6 +394,36 @@ export const secretImportServiceFactory = ({
return { message: "replication started" }; return { message: "replication started" };
}; };
const getProjectImportCount = async ({
path: secretPath,
environment,
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
search
}: TGetSecretImportsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new NotFoundError({ message: "Folder not found", name: "Get imports" });
const count = await secretImportDAL.getProjectImportCount({ folderId: folder.id, search });
return count;
};
const getImports = async ({ const getImports = async ({
path: secretPath, path: secretPath,
environment, environment,
@@ -401,7 +431,10 @@ export const secretImportServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
search,
limit,
offset
}: TGetSecretImportsDTO) => { }: TGetSecretImportsDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -418,7 +451,7 @@ export const secretImportServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" });
const secImports = await secretImportDAL.find({ folderId: folder.id }); const secImports = await secretImportDAL.find({ folderId: folder.id, search, limit, offset });
return secImports; return secImports;
}; };
@@ -512,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) => ({
@@ -531,6 +568,7 @@ export const secretImportServiceFactory = ({
getSecretsFromImports, getSecretsFromImports,
getRawSecretsFromImports, getRawSecretsFromImports,
resyncSecretImportReplication, resyncSecretImportReplication,
getProjectImportCount,
fnSecretsFromImports fnSecretsFromImports
}; };
}; };
@@ -32,6 +32,9 @@ export type TDeleteSecretImportDTO = {
export type TGetSecretImportsDTO = { export type TGetSecretImportsDTO = {
environment: string; environment: string;
path: string; path: string;
search?: string;
limit?: number;
offset?: number;
} & TProjectPermission; } & TProjectPermission;
export type TGetSecretsFromImportDTO = { export type TGetSecretsFromImportDTO = {
@@ -5,6 +5,8 @@ import { TDbClient } from "@app/db";
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas"; import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>; export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
@@ -181,7 +183,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
}; };
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { // get unique secret count by folder IDs
const countByFolderIds = async (
folderIds: string[],
userId?: string,
tx?: Knex,
filters?: {
search?: string;
tagSlugs?: string[];
}
) => {
try { try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) { if (userId && !uuidValidate(userId)) {
@@ -189,8 +200,70 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
userId = undefined; userId = undefined;
} }
const secs = await (tx || db.replicaNode())(TableName.SecretV2) const query = (tx || db.replicaNode())(TableName.SecretV2)
.whereIn("folderId", folderIds) .whereIn("folderId", folderIds)
.where((bd) => {
if (filters?.search) {
void bd.whereILike("key", `%${filters?.search}%`);
}
})
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
})
.countDistinct("key");
// only need to join tags if filtering by tag slugs
const slugs = filters?.tagSlugs?.filter(Boolean);
if (slugs && slugs.length > 0) {
void query
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.whereIn("slug", slugs);
}
const secrets = await query;
return Number(secrets[0]?.count ?? 0);
} catch (error) {
throw new DatabaseError({ error, name: "get folder secret count" });
}
};
const findByFolderIds = async (
folderIds: string[],
userId?: string,
tx?: Knex,
filters?: {
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
tagSlugs?: string[];
}
) => {
try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) {
// eslint-disable-next-line no-param-reassign
userId = undefined;
}
const query = (tx || db.replicaNode())(TableName.SecretV2)
.whereIn("folderId", folderIds)
.where((bd) => {
if (filters?.search) {
void bd.whereILike("key", `%${filters?.search}%`);
}
})
.where((bd) => { .where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null }); void bd.whereNull("userId").orWhere({ userId: userId || null });
}) })
@@ -204,11 +277,37 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id` `${TableName.SecretTag}.id`
) )
.select(selectAllTableCols(TableName.SecretV2)) .select(
selectAllTableCols(TableName.SecretV2),
db.raw(`DENSE_RANK() OVER (ORDER BY "key" ${filters?.orderDirection ?? OrderByDirection.ASC}) as rank`)
)
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.orderBy("id", "asc"); .where((bd) => {
const slugs = filters?.tagSlugs?.filter(Boolean);
if (slugs && slugs.length > 0) {
void bd.whereIn("slug", slugs);
}
})
.orderBy(
filters?.orderBy === SecretsOrderBy.Name ? "key" : "id",
filters?.orderDirection ?? OrderByDirection.ASC
);
let secs: Awaited<typeof query>;
if (filters?.limit) {
const rankOffset = (filters?.offset ?? 0) + 1; // ranks start at 1
secs = await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", rankOffset)
.andWhere("w.rank", "<", rankOffset + filters.limit);
} else {
secs = await query;
}
const data = sqlNestRelationships({ const data = sqlNestRelationships({
data: secs, data: secs,
@@ -384,6 +483,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
findBySecretKeys, findBySecretKeys,
upsertSecretReferences, upsertSecretReferences,
findReferencedSecretReferences, findReferencedSecretReferences,
findAllProjectSecretValues findAllProjectSecretValues,
countByFolderIds
}; };
}; };
@@ -59,7 +59,7 @@ type TSecretV2BridgeServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
>; >;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
@@ -431,6 +431,165 @@ export const secretV2BridgeServiceFactory = ({
}); });
}; };
// get unique secrets count for multiple envs
const getSecretsCountMultiEnv = async ({
actorId,
path,
projectId,
actor,
actorOrgId,
actorAuthMethod,
environments,
...params
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
environments: string[];
}) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
// verify user has access to all environments
environments.forEach((environment) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
)
);
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
if (!folders.length) return 0;
const count = await secretDAL.countByFolderIds(
folders.map((folder) => folder.id),
actorId,
undefined,
params
);
return count;
};
// get secret count for individual env
const getSecretsCount = async ({
actorId,
path,
environment,
projectId,
actor,
actorOrgId,
actorAuthMethod,
...params
}: Pick<
TGetSecretsDTO,
| "actorId"
| "actor"
| "path"
| "projectId"
| "actorOrgId"
| "actorAuthMethod"
| "tagSlugs"
| "environment"
| "search"
>) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return 0;
const count = await secretDAL.countByFolderIds([folder.id], actorId, undefined, params);
return count;
};
// get secrets for multiple envs
const getSecretsMultiEnv = async ({
actorId,
path,
environments,
projectId,
actor,
actorOrgId,
actorAuthMethod,
...params
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
environments: string[];
}) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
let paths: { folderId: string; path: string; environment: string }[] = [];
// verify user has access to all environments
environments.forEach((environment) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
)
);
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
if (!folders.length) {
return [];
}
paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug }));
const groupedPaths = groupBy(paths, (p) => p.folderId);
const secrets = await secretDAL.findByFolderIds(
paths.map((p) => p.folderId),
actorId,
undefined,
params
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedSecrets = secrets.map((secret) =>
reshapeBridgeSecret(
projectId,
groupedPaths[secret.folderId][0].environment,
groupedPaths[secret.folderId][0].path,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
}
)
);
return decryptedSecrets;
};
const getSecrets = async ({ const getSecrets = async ({
actorId, actorId,
path, path,
@@ -441,8 +600,8 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
includeImports, includeImports,
recursive, recursive,
tagSlugs = [], expandSecretReferences: shouldExpandSecretReferences,
expandSecretReferences: shouldExpandSecretReferences ...params
}: TGetSecretsDTO) => { }: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -490,7 +649,9 @@ export const secretV2BridgeServiceFactory = ({
const secrets = await secretDAL.findByFolderIds( const secrets = await secretDAL.findByFolderIds(
paths.map((p) => p.folderId), paths.map((p) => p.folderId),
actorId actorId,
undefined,
params
); );
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
@@ -509,9 +670,7 @@ export const secretV2BridgeServiceFactory = ({
: "" : ""
}) })
); );
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const expandSecretReferences = expandSecretReferencesFactory({ const expandSecretReferences = expandSecretReferencesFactory({
projectId, projectId,
folderDAL, folderDAL,
@@ -520,7 +679,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (shouldExpandSecretReferences) { if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath); const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
await Promise.allSettled( await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) => Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled( Promise.allSettled(
@@ -541,7 +700,7 @@ export const secretV2BridgeServiceFactory = ({
if (!includeImports) { if (!includeImports) {
return { return {
secrets: filteredSecrets secrets: decryptedSecrets
}; };
} }
@@ -569,7 +728,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
return { return {
secrets: filteredSecrets, secrets: decryptedSecrets,
imports: importedSecrets imports: importedSecrets
}; };
}; };
@@ -1416,6 +1575,9 @@ export const secretV2BridgeServiceFactory = ({
getSecrets, getSecrets,
getSecretVersions, getSecretVersions,
backfillSecretReferences, backfillSecretReferences,
moveSecrets moveSecrets,
getSecretsCount,
getSecretsCountMultiEnv,
getSecretsMultiEnv
}; };
}; };
@@ -1,8 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
@@ -21,6 +22,11 @@ export type TGetSecretsDTO = {
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
offset?: number;
limit?: number;
search?: string;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretDTO = { export type TGetASecretDTO = {
+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" });
+399 -246
View File
@@ -1,7 +1,15 @@
/* 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 { 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";
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
@@ -21,6 +29,7 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service"; import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
@@ -40,13 +49,16 @@ import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns"; import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { TSecretDALFactory } from "./secret-dal"; 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";
@@ -71,6 +83,7 @@ type TSecretQueueFactoryDep = {
secretVersionDAL: TSecretVersionDALFactory; secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
userDAL: Pick<TUserDALFactory, "findById">;
secretVersionTagDAL: TSecretVersionTagDALFactory; secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: TSecretV2BridgeDALFactory; secretV2BridgeDAL: TSecretV2BridgeDALFactory;
@@ -81,6 +94,7 @@ type TSecretQueueFactoryDep = {
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">; snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">; snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
}; };
export type TGetSecrets = { export type TGetSecrets = {
@@ -106,6 +120,7 @@ export const secretQueueFactory = ({
secretDAL, secretDAL,
secretImportDAL, secretImportDAL,
folderDAL, folderDAL,
userDAL,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
orgDAL, orgDAL,
@@ -125,7 +140,8 @@ export const secretQueueFactory = ({
snapshotDAL, snapshotDAL,
snapshotSecretV2BridgeDAL, snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL, secretApprovalRequestDAL,
keyStore keyStore,
auditLogService
}: TSecretQueueFactoryDep) => { }: TSecretQueueFactoryDep) => {
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -430,7 +446,9 @@ export const secretQueueFactory = ({
return content; return content;
}; };
const syncIntegrations = async (dto: TGetSecrets & { deDupeQueue?: Record<string, boolean> }) => { const syncIntegrations = async (
dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> }
) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 3, attempts: 3,
delay: 1000, delay: 1000,
@@ -499,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,
@@ -528,7 +559,7 @@ export const secretQueueFactory = ({
} }
} }
); );
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue }); await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
if (!excludeReplication) { if (!excludeReplication) {
await replicateSecrets({ await replicateSecrets({
_deDupeReplicationQueue: deDupeReplicationQueue, _deDupeReplicationQueue: deDupeReplicationQueue,
@@ -544,274 +575,396 @@ export const secretQueueFactory = ({
}); });
queueService.start(QueueName.IntegrationSync, async (job) => { queueService.start(QueueName.IntegrationSync, async (job) => {
const { environment, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data; if (job.name === QueueJobs.SendFailedIntegrationSyncEmails) {
if (depth > MAX_SYNC_SECRET_DEPTH) return; const appCfg = getConfig();
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const jobPayload = job.data as TFailedIntegrationSyncEmailsPayload;
if (!folder) {
throw new Error("Secret path not found"); 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}`
}
});
} }
// find all imports made with the given environment and secret path if (job.name === QueueJobs.IntegrationSync) {
const linkSourceDto = { const {
projectId, environment,
importEnv: folder.environment.id, actorId,
importPath: secretPath, isManual,
isReplication: false
};
const imports = await secretImportDAL.find(linkSourceDto);
if (imports.length) {
// keep calling sync secret for all the imports made
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
logger.info(
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
imports
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueSecretQueueKey(
foldersGroupedById[folderId][0]?.environmentSlug as string,
foldersGroupedById[folderId][0]?.path as string
)
]
)
.map(({ folderId }) =>
syncSecrets({
projectId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
);
}
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
let referencedFolderIds;
if (shouldUseSecretV2Bridge) {
const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences(
projectId, projectId,
folder.environment.slug, secretPath,
secretPath depth = 1,
); deDupeQueue = {}
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId); } = job.data as TIntegrationSyncPayload;
} else { if (depth > MAX_SYNC_SECRET_DEPTH) return;
const secretReferences = await secretDAL.findReferencedSecretReferences(
projectId,
folder.environment.slug,
secretPath
);
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
}
if (referencedFolderIds.length) {
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
logger.info(
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
referencedFolderIds
.filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
// filter out already synced ones
.filter(
(folderId) =>
!deDupeQueue[
uniqueSecretQueueKey(
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
referencedFoldersGroupedById[folderId][0]?.path as string
)
]
)
.map((folderId) =>
syncSecrets({
projectId,
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
);
}
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
const toBeSyncedIntegrations = integrations.filter( if (!folder) {
// note: sync only the integrations sourced from secretPath throw new Error("Secret path not found");
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
);
if (!integrations.length) return;
logger.info(
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
);
const lock = await keyStore.acquireLock(
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
10000,
{
retryCount: 3,
retryDelay: 2000
} }
);
const lockAcquiredTime = new Date();
const lastRunSyncIntegrationTimestamp = await keyStore.getItem( // find all imports made with the given environment and secret path
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath) const linkSourceDto = {
); projectId,
importEnv: folder.environment.id,
importPath: secretPath,
isReplication: false
};
const imports = await secretImportDAL.find(linkSourceDto);
// check whether the integration should wait or not if (imports.length) {
if (lastRunSyncIntegrationTimestamp) { // keep calling sync secret for all the imports made
const INTEGRATION_INTERVAL = 2000; const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp); const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
if (isStaleSyncIntegration) { const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
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: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
imports
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueSecretQueueKey(
foldersGroupedById[folderId][0]?.environmentSlug as string,
foldersGroupedById[folderId][0]?.path as string
)
]
)
.map(({ folderId }) =>
syncSecrets({
projectId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
);
}
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
let referencedFolderIds;
if (shouldUseSecretV2Bridge) {
const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences(
projectId,
folder.environment.slug,
secretPath
);
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
} else {
const secretReferences = await secretDAL.findReferencedSecretReferences(
projectId,
folder.environment.slug,
secretPath
);
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
}
if (referencedFolderIds.length) {
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
logger.info(
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
referencedFolderIds
.filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
// filter out already synced ones
.filter(
(folderId) =>
!deDupeQueue[
uniqueSecretQueueKey(
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
referencedFoldersGroupedById[folderId][0]?.path as string
)
]
)
.map((folderId) =>
syncSecrets({
projectId,
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
); );
return;
} }
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds( const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
lockAcquiredTime.toISOString(), const toBeSyncedIntegrations = integrations.filter(
lastRunSyncIntegrationTimestamp // note: sync only the integrations sourced from secretPath
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
); );
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
await new Promise((resolve) => {
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
});
}
// akhilmhdh: this try catch is for lock release const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = [];
try {
const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2({
environment,
projectId,
folderId: folder.id,
depth: 1,
secretPath,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
})
: await getIntegrationSecrets({
environment,
projectId,
folderId: folder.id,
key: botKey as string,
depth: 1,
secretPath
});
for (const integration of toBeSyncedIntegrations) { if (!integrations.length) return;
const integrationAuth = { logger.info(
...integration.integrationAuth, `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
createdAt: new Date(), );
updatedAt: new Date(),
projectId: integration.projectId
};
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( const lock = await keyStore.acquireLock(
integrationAuth, [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
shouldUseSecretV2Bridge, 10000,
botKey {
); retryCount: 3,
let awsAssumeRoleArn = null; retryDelay: 2000
if (shouldUseSecretV2Bridge) {
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
awsAssumeRoleArn = secretManagerDecryptor({
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
}).toString();
}
} else if (
integrationAuth.awsAssumeIamRoleArnTag &&
integrationAuth.awsAssumeIamRoleArnIV &&
integrationAuth.awsAssumeIamRoleArnCipherText
) {
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
iv: integrationAuth.awsAssumeIamRoleArnIV,
tag: integrationAuth.awsAssumeIamRoleArnTag,
key: botKey as string
});
} }
);
const lockAcquiredTime = new Date();
const suffixedSecrets: typeof secrets = {}; const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
const metadata = integration.metadata as Record<string, string>; KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
if (metadata) { );
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
// akhilmhdh: this try catch is for catching integration error and saving it in db // check whether the integration should wait or not
try { if (lastRunSyncIntegrationTimestamp) {
// akhilmhdh: this needs to changed later to be more easier to use const INTEGRATION_INTERVAL = 2000;
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
const response = await syncIntegrationSecrets({ if (isStaleSyncIntegration) {
createManySecretsRawFn, logger.info(
updateManySecretsRawFn, `getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
integrationDAL,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: accessId as string,
awsAssumeRoleArn,
accessToken,
projectId,
appendices: {
prefix: metadata?.secretPrefix || "",
suffix: metadata?.secretSuffix || ""
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: response?.syncMessage ?? "",
isSynced: response?.isSynced ?? true
});
} catch (err) {
logger.error(
err,
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
); );
return;
}
const message = const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || lockAcquiredTime.toISOString(),
"Unknown error occurred."; lastRunSyncIntegrationTimestamp
);
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
await new Promise((resolve) => {
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
});
}
await integrationDAL.updateById(integration.id, { const generateActor = async (): Promise<Actor> => {
lastSyncJobId: job.id, if (isManual && actorId) {
lastUsed: new Date(), const user = await userDAL.findById(actorId);
syncMessage: message,
isSynced: false if (!user) {
throw new Error("User not found");
}
return {
type: ActorType.USER,
metadata: {
email: user.email,
username: user.username,
userId: user.id
}
};
}
return {
type: ActorType.PLATFORM,
metadata: {}
};
};
// akhilmhdh: this try catch is for lock release
try {
const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2({
environment,
projectId,
folderId: folder.id,
depth: 1,
secretPath,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
})
: await getIntegrationSecrets({
environment,
projectId,
folderId: folder.id,
key: botKey as string,
depth: 1,
secretPath
});
for (const integration of toBeSyncedIntegrations) {
const integrationAuth = {
...integration.integrationAuth,
createdAt: new Date(),
updatedAt: new Date(),
projectId: integration.projectId
};
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
shouldUseSecretV2Bridge,
botKey
);
let awsAssumeRoleArn = null;
if (shouldUseSecretV2Bridge) {
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
awsAssumeRoleArn = secretManagerDecryptor({
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
}).toString();
}
} else if (
integrationAuth.awsAssumeIamRoleArnTag &&
integrationAuth.awsAssumeIamRoleArnIV &&
integrationAuth.awsAssumeIamRoleArnCipherText
) {
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
iv: integrationAuth.awsAssumeIamRoleArnIV,
tag: integrationAuth.awsAssumeIamRoleArnTag,
key: botKey as string
});
}
const suffixedSecrets: typeof secrets = {};
const metadata = integration.metadata as Record<string, string>;
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
// akhilmhdh: this try catch is for catching integration error and saving it in db
try {
// akhilmhdh: this needs to changed later to be more easier to use
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
const response = await syncIntegrationSecrets({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: accessId as string,
awsAssumeRoleArn,
accessToken,
projectId,
appendices: {
prefix: metadata?.secretPrefix || "",
suffix: metadata?.secretSuffix || ""
}
});
await auditLogService.createAuditLog({
projectId,
actor: await generateActor(),
event: {
type: EventType.INTEGRATION_SYNCED,
metadata: {
integrationId: integration.id,
isSynced: response?.isSynced ?? true,
lastSyncJobId: job?.id ?? "",
lastUsed: new Date(),
syncMessage: response?.syncMessage ?? ""
}
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: response?.syncMessage ?? "",
isSynced: response?.isSynced ?? true
});
// May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check.
if (response?.isSynced === false) {
integrationsFailedToSync.push({
integrationId: integration.id,
syncMessage: response.syncMessage
});
}
} catch (err) {
logger.error(
err,
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]`
);
const message =
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
"Unknown error occurred.";
await auditLogService.createAuditLog({
projectId,
actor: await generateActor(),
event: {
type: EventType.INTEGRATION_SYNCED,
metadata: {
integrationId: integration.id,
isSynced: false,
lastSyncJobId: job?.id ?? "",
lastUsed: new Date(),
syncMessage: message
}
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
syncMessage: message,
isSynced: false
});
integrationsFailedToSync.push({
integrationId: integration.id,
syncMessage: message
});
}
}
} finally {
await lock.release();
if (integrationsFailedToSync.length) {
await sendFailedIntegrationSyncEmails({
count: integrationsFailedToSync.length,
environmentName: folder.environment.name,
environmentSlug: environment,
...(isManual &&
actorId && {
manuallyTriggeredByUserId: actorId
}),
projectId,
secretPath,
syncMessage: integrationsFailedToSync[0].syncMessage
}); });
} }
} }
} finally {
await lock.release();
}
await keyStore.setItemWithExpiry( await keyStore.setItemWithExpiry(
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath), KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath),
KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds, KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds,
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 }) => {
+169 -13
View File
@@ -954,6 +954,120 @@ export const secretServiceFactory = ({
return secretsDeleted; return secretsDeleted;
}; };
const getSecretsCount = async ({
projectId,
path,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environment,
tagSlugs = [],
...v2Params
}: Pick<
TGetSecretsRawDTO,
| "projectId"
| "path"
| "actor"
| "actorId"
| "actorOrgId"
| "actorAuthMethod"
| "environment"
| "tagSlugs"
| "search"
>) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge)
throw new BadRequestError({
message: "Project version does not support pagination",
name: "pagination_not_supported"
});
const count = await secretV2BridgeService.getSecretsCount({
projectId,
actorId,
actor,
actorOrgId,
environment,
path,
actorAuthMethod,
tagSlugs,
...v2Params
});
return count;
};
const getSecretsCountMultiEnv = async ({
projectId,
path,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environments,
...v2Params
}: Pick<
TGetSecretsRawDTO,
"projectId" | "path" | "actor" | "actorId" | "actorOrgId" | "actorAuthMethod" | "search"
> & { environments: string[] }) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge)
throw new BadRequestError({
message: "Project version does not support pagination",
name: "pagination_not_supported"
});
const count = await secretV2BridgeService.getSecretsCountMultiEnv({
projectId,
actorId,
actor,
actorOrgId,
environments,
path,
actorAuthMethod,
...v2Params
});
return count;
};
const getSecretsRawMultiEnv = async ({
projectId,
path,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environments,
...params
}: Omit<TGetSecretsRawDTO, "environment" | "includeImports" | "expandSecretReferences" | "recursive" | "tagSlugs"> & {
environments: string[];
}) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge)
throw new BadRequestError({
message: "Project version does not support pagination",
name: "pagination_not_supported"
});
const secrets = await secretV2BridgeService.getSecretsMultiEnv({
projectId,
actorId,
actor,
actorOrgId,
environments,
path,
actorAuthMethod,
...params
});
return secrets;
};
const getSecretsRaw = async ({ const getSecretsRaw = async ({
projectId, projectId,
path, path,
@@ -965,7 +1079,8 @@ export const secretServiceFactory = ({
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
recursive, recursive,
tagSlugs = [] tagSlugs = [],
...paramsV2
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
@@ -980,12 +1095,17 @@ export const secretServiceFactory = ({
recursive, recursive,
actorAuthMethod, actorAuthMethod,
includeImports, includeImports,
tagSlugs tagSlugs,
...paramsV2
}); });
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,
@@ -1146,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) {
@@ -1238,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);
@@ -1376,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);
@@ -1498,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,
@@ -1598,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);
@@ -1720,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,
@@ -1848,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({
@@ -2182,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);
@@ -2265,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);
@@ -2656,6 +2809,9 @@ export const secretServiceFactory = ({
getSecretVersions, getSecretVersions,
backfillSecretReferences, backfillSecretReferences,
moveSecrets, moveSecrets,
startSecretV2Migration startSecretV2Migration,
getSecretsCount,
getSecretsCountMultiEnv,
getSecretsRawMultiEnv
}; };
}; };
+36 -1
View File
@@ -1,7 +1,8 @@
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 { TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
@@ -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;
@@ -81,6 +105,8 @@ export type TGetSecretsDTO = {
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
limit?: number;
offset?: number;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretDTO = { export type TGetASecretDTO = {
@@ -143,6 +169,10 @@ export type TDeleteBulkSecretDTO = {
}>; }>;
} & TProjectPermission; } & TProjectPermission;
export enum SecretsOrderBy {
Name = "name" // "key" for secrets but using name for use across resources
}
export type TGetSecretsRawDTO = { export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
path: string; path: string;
@@ -150,6 +180,11 @@ export type TGetSecretsRawDTO = {
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
offset?: number;
limit?: number;
search?: string;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretRawDTO = { export type TGetASecretRawDTO = {
+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
+3 -3
View File
@@ -160,19 +160,19 @@ 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()
} }
environmentName, _ := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") { if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile() environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" { if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace environmentName = environmentFromWorkspace
} }
} }
projectId, err := cmd.Flags().GetString("projectId") projectId, err := cmd.Flags().GetString("projectId")
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
+3 -2
View File
@@ -63,8 +63,9 @@ 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 {
+10 -4
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)
} }
} }
@@ -101,14 +105,16 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
if strings.HasPrefix(infisicalToken, "st.") { if strings.HasPrefix(infisicalToken, "st.") {
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
} }
@@ -1,4 +1,4 @@
--- ---
title: "Export" title: "Export"
openapi: "GET /api/v1/workspace/{workspaceId}/audit-logs" openapi: "GET /api/v1/organization/audit-logs"
--- ---
@@ -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>
); );
@@ -100,7 +100,7 @@ export default function NavHeader({
onValueChange={(value) => { onValueChange={(value) => {
if (value && onEnvChange) onEnvChange(value); if (value && onEnvChange) onEnvChange(value);
}} }}
className="bg-transparent pl-0 text-sm font-medium text-primary/80 hover:text-primary" className="border-none bg-transparent pl-0 text-sm font-medium text-primary/80 hover:text-primary"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl" dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
> >
{userAvailableEnvs?.map(({ name, slug }) => ( {userAvailableEnvs?.map(({ name, slug }) => (
@@ -1,3 +1,4 @@
import { ReactElement } from "react";
import { import {
faCaretDown, faCaretDown,
faCheck, faCheck,
@@ -23,6 +24,7 @@ export type PaginationProps = {
onChangePerPage: (newRows: number) => void; onChangePerPage: (newRows: number) => void;
className?: string; className?: string;
perPageList?: number[]; perPageList?: number[];
startAdornment?: ReactElement;
}; };
export const Pagination = ({ export const Pagination = ({
@@ -32,7 +34,8 @@ export const Pagination = ({
onChangePage, onChangePage,
onChangePerPage, onChangePerPage,
perPageList = [10, 20, 50, 100], perPageList = [10, 20, 50, 100],
className className,
startAdornment
}: PaginationProps) => { }: PaginationProps) => {
const prevPageNumber = Math.max(1, page - 1); const prevPageNumber = Math.max(1, page - 1);
const canGoPrev = page > 1; const canGoPrev = page > 1;
@@ -46,11 +49,12 @@ export const Pagination = ({
return ( return (
<div <div
className={twMerge( className={twMerge(
"flex w-full items-center justify-end bg-mineshaft-800 py-3 px-4 text-white", "flex w-full items-center justify-end bg-mineshaft-800 py-3 px-4 text-white",
className className
)} )}
> >
<div className="mr-6 flex items-center space-x-2"> {startAdornment}
<div className="ml-auto mr-6 flex items-center space-x-2">
<div className="text-xs"> <div className="text-xs">
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count} {(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
</div> </div>
@@ -21,7 +21,8 @@ export enum OrgPermissionSubjects {
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity", Identity = "identity",
Kms = "kms", Kms = "kms",
AdminConsole = "organization-admin-console" AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs"
} }
export enum OrgPermissionAdminConsoleAction { export enum OrgPermissionAdminConsoleAction {
@@ -43,6 +44,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs];
export type TOrgPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -25,6 +25,21 @@ export const UserProvider = ({ children }: Props): JSX.Element => {
}; };
}, [data, isLoading]); }, [data, isLoading]);
if (isLoading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
decoding="async"
loading="lazy"
alt="infisical loading indicator"
/>
</div>
);
}
return <UserContext.Provider value={value}>{children}</UserContext.Provider>; return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}; };
@@ -1,6 +1,7 @@
import { createContext, ReactNode, useContext, useMemo } from "react"; import { createContext, ReactNode, useContext, useEffect, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications";
import { useGetUserWorkspaces } from "@app/hooks/api"; import { useGetUserWorkspaces } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/workspace/types"; import { Workspace } from "@app/hooks/api/workspace/types";
@@ -31,6 +32,34 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
}; };
}, [ws, workspaceId, isLoading]); }, [ws, workspaceId, isLoading]);
const shouldTriggerNoProjectAccess =
!value.isLoading &&
!value.currentWorkspace &&
router.pathname.startsWith("/project") &&
workspaceId;
// handle redirects for project-specific routes
useEffect(() => {
if (shouldTriggerNoProjectAccess) {
createNotification({
text: "You are not a member of this project.",
type: "info"
});
setTimeout(() => {
router.push("/");
}, 5000);
}
}, [shouldTriggerNoProjectAccess, router]);
if (shouldTriggerNoProjectAccess) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800 text-primary-50">
You do not have sufficient access to this project.
</div>
);
}
return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>; return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>;
}; };
@@ -79,7 +79,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]: [EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
"Update certificate template EST configuration", "Update certificate template EST configuration",
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration", [EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration" [EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
[EventType.INTEGRATION_SYNCED]: "Integration sync"
}; };
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+3 -1
View File
@@ -1,4 +1,5 @@
export enum ActorType { export enum ActorType {
PLATFORM = "platform",
USER = "user", USER = "user",
SERVICE = "service", SERVICE = "service",
IDENTITY = "identity" IDENTITY = "identity"
@@ -91,5 +92,6 @@ export enum EventType {
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config" GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
} }
+39 -16
View File
@@ -1,35 +1,58 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useInfiniteQuery, UseInfiniteQueryOptions, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { Actor, AuditLog, AuditLogFilters } from "./types"; import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
export const auditLogKeys = { export const auditLogKeys = {
getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) => getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) =>
[{ workspaceId, filters }, "audit-logs"] as const, [{ workspaceId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (workspaceId: string) => getAuditLogActorFilterOpts: (workspaceId: string) =>
[{ workspaceId }, "audit-log-actor-filters"] as const [{ workspaceId }, "audit-log-actor-filters"] as const
}; };
export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => { export const useGetAuditLogs = (
filters: TGetAuditLogsFilter,
projectId: string | null,
options: Omit<
UseInfiniteQueryOptions<
AuditLog[],
unknown,
AuditLog[],
AuditLog[],
ReturnType<typeof auditLogKeys.getAuditLogs>
>,
"queryFn" | "queryKey" | "getNextPageParam"
> = {}
) => {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: auditLogKeys.getAuditLogs(workspaceId, filters), queryKey: auditLogKeys.getAuditLogs(projectId, filters),
queryFn: async ({ pageParam }) => { queryFn: async ({ pageParam }) => {
const auditLogEndpoint = workspaceId const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
? `/api/v1/workspace/${workspaceId}/audit-logs` "/api/v1/organization/audit-logs",
: "/api/v1/organization/audit-logs"; {
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, { params: {
params: { ...filters,
...filters, offset: pageParam,
offset: pageParam, startDate: filters?.startDate?.toISOString(),
startDate: filters?.startDate?.toISOString(), endDate: filters?.endDate?.toISOString(),
endDate: filters?.endDate?.toISOString() ...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)
.map(([key, value]) => `${key}=${value}`)
.join(",")
}
: {}),
...(filters.eventType?.length ? { eventType: filters.eventType.join(",") } : {}),
...(projectId ? { projectId } : {})
}
} }
}); );
return data.auditLogs; return data.auditLogs;
}, },
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined lastPage.length !== 0 ? pages.length * filters.limit : undefined,
...options
}); });
}; };
+38 -12
View File
@@ -3,6 +3,18 @@ import { IdentityTrustedIp } from "../identities/types";
import { PkiItemType } from "../pkiCollections/constants"; import { PkiItemType } from "../pkiCollections/constants";
import { ActorType, EventType, UserAgentType } from "./enums"; import { ActorType, EventType, UserAgentType } from "./enums";
export type TGetAuditLogsFilter = {
eventType?: EventType[];
userAgentType?: UserAgentType;
eventMetadata?: Record<string, string>;
actorType?: ActorType;
projectId?: string;
actorId?: string; // user ID format
startDate?: Date;
endDate?: Date;
limit: number;
};
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
email: string; email: string;
@@ -33,7 +45,13 @@ export interface IdentityActor {
metadata: IdentityActorMetadata; metadata: IdentityActorMetadata;
} }
export type Actor = UserActor | ServiceActor | IdentityActor; export interface PlatformActorMetadata {}
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@@ -761,6 +779,22 @@ interface GetProjectSlackConfig {
}; };
} }
export enum IntegrationSyncedEventTrigger {
MANUAL = "manual",
AUTO = "auto"
}
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -838,7 +872,8 @@ export type Event =
| CreateCertificateTemplateEstConfig | CreateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig | GetCertificateTemplateEstConfig
| UpdateProjectSlackConfig | UpdateProjectSlackConfig
| GetProjectSlackConfig; | GetProjectSlackConfig
| IntegrationSyncedEvent;
export type AuditLog = { export type AuditLog = {
id: string; id: string;
@@ -851,17 +886,8 @@ export type AuditLog = {
userAgentType: UserAgentType; userAgentType: UserAgentType;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
project: { project?: {
name: string; name: string;
slug: string; slug: string;
}; };
}; };
export type AuditLogFilters = {
eventType?: EventType;
userAgentType?: UserAgentType;
actor?: string;
limit: number;
startDate?: Date;
endDate?: Date;
};
@@ -0,0 +1 @@
export { useGetProjectSecretsDetails } from "./queries";
@@ -0,0 +1,261 @@
import { useCallback } from "react";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import axios from "axios";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import {
DashboardProjectSecretsDetails,
DashboardProjectSecretsDetailsResponse,
DashboardProjectSecretsOverview,
DashboardProjectSecretsOverviewResponse,
DashboardSecretsOrderBy,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO
} from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
export const dashboardKeys = {
all: () => ["dashboard"] as const,
getDashboardSecrets: ({
projectId,
secretPath
}: Pick<TGetDashboardProjectSecretsDetailsDTO, "projectId" | "secretPath">) =>
[...dashboardKeys.all(), { projectId, secretPath }] as const,
getProjectSecretsOverview: ({
projectId,
secretPath,
...params
}: TGetDashboardProjectSecretsOverviewDTO) =>
[
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
"secrets-overview",
params
] as const,
getProjectSecretsDetails: ({
projectId,
secretPath,
environment,
...params
}: TGetDashboardProjectSecretsDetailsDTO) =>
[
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
environment,
"secrets-details",
params
] as const
};
export const fetchProjectSecretsOverview = async ({
includeFolders,
includeSecrets,
includeDynamicSecrets,
environments,
...params
}: TGetDashboardProjectSecretsOverviewDTO) => {
const { data } = await apiRequest.get<DashboardProjectSecretsOverviewResponse>(
"/api/v3/dashboard/secrets-overview",
{
params: {
...params,
environments: encodeURIComponent(environments.join(",")),
includeFolders: includeFolders ? "1" : "",
includeSecrets: includeSecrets ? "1" : "",
includeDynamicSecrets: includeDynamicSecrets ? "1" : ""
}
}
);
return data;
};
export const fetchProjectSecretsDetails = async ({
includeFolders,
includeImports,
includeSecrets,
includeDynamicSecrets,
tags,
...params
}: TGetDashboardProjectSecretsDetailsDTO) => {
const { data } = await apiRequest.get<DashboardProjectSecretsDetailsResponse>(
"/api/v3/dashboard/secrets-details",
{
params: {
...params,
includeImports: includeImports ? "1" : "",
includeFolders: includeFolders ? "1" : "",
includeSecrets: includeSecrets ? "1" : "",
includeDynamicSecrets: includeDynamicSecrets ? "1" : "",
tags: encodeURIComponent(
Object.entries(tags)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([_, enabled]) => enabled)
.map(([tag]) => tag)
.join(",")
)
}
}
);
return data;
};
export const useGetProjectSecretsOverview = (
{
projectId,
secretPath,
offset = 0,
limit = 100,
orderBy = DashboardSecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = "",
includeSecrets,
includeFolders,
includeDynamicSecrets,
environments
}: TGetDashboardProjectSecretsOverviewDTO,
options?: Omit<
UseQueryOptions<
DashboardProjectSecretsOverviewResponse,
unknown,
DashboardProjectSecretsOverview,
ReturnType<typeof dashboardKeys.getProjectSecretsOverview>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
...options,
// wait for all values to be available
enabled: Boolean(projectId) && (options?.enabled ?? true) && Boolean(environments.length),
queryKey: dashboardKeys.getProjectSecretsOverview({
secretPath,
search,
limit,
orderBy,
orderDirection,
offset,
projectId,
includeSecrets,
includeFolders,
includeDynamicSecrets,
environments
}),
queryFn: () =>
fetchProjectSecretsOverview({
secretPath,
search,
limit,
orderBy,
orderDirection,
offset,
projectId,
includeSecrets,
includeFolders,
includeDynamicSecrets,
environments
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
createNotification({
title: "Error fetching secret details",
type: "error",
text: serverResponse.message
});
}
},
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
const { secrets, ...select } = data;
return {
...select,
secrets: secrets ? mergePersonalSecrets(secrets) : undefined
};
}, []),
keepPreviousData: true
});
};
export const useGetProjectSecretsDetails = (
{
projectId,
secretPath,
environment,
offset = 0,
limit = 100,
orderBy = DashboardSecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = "",
includeSecrets,
includeFolders,
includeImports,
includeDynamicSecrets,
tags
}: TGetDashboardProjectSecretsDetailsDTO,
options?: Omit<
UseQueryOptions<
DashboardProjectSecretsDetailsResponse,
unknown,
DashboardProjectSecretsDetails,
ReturnType<typeof dashboardKeys.getProjectSecretsDetails>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
...options,
// wait for all values to be available
enabled: Boolean(projectId) && (options?.enabled ?? true),
queryKey: dashboardKeys.getProjectSecretsDetails({
secretPath,
search,
limit,
orderBy,
orderDirection,
offset,
projectId,
environment,
includeSecrets,
includeFolders,
includeImports,
includeDynamicSecrets,
tags
}),
queryFn: () =>
fetchProjectSecretsDetails({
secretPath,
search,
limit,
orderBy,
orderDirection,
offset,
projectId,
environment,
includeSecrets,
includeFolders,
includeImports,
includeDynamicSecrets,
tags
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
createNotification({
title: "Error fetching secret details",
type: "error",
text: serverResponse.message
});
}
},
select: useCallback(
(data: Awaited<ReturnType<typeof fetchProjectSecretsDetails>>) => ({
...data,
secrets: data.secrets ? mergePersonalSecrets(data.secrets) : undefined
}),
[]
),
keepPreviousData: true
});
};
+68
View File
@@ -0,0 +1,68 @@
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
import { TSecretImport } from "@app/hooks/api/secretImports/types";
import { SecretV3Raw, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
export type DashboardProjectSecretsOverviewResponse = {
folders?: (TSecretFolder & { environment: string })[];
dynamicSecrets?: (TDynamicSecret & { environment: string })[];
secrets?: SecretV3Raw[];
totalSecretCount?: number;
totalFolderCount?: number;
totalDynamicSecretCount?: number;
totalCount: number;
};
export type DashboardProjectSecretsDetailsResponse = {
imports?: TSecretImport[];
folders?: TSecretFolder[];
dynamicSecrets?: TDynamicSecret[];
secrets?: SecretV3Raw[];
totalImportCount?: number;
totalFolderCount?: number;
totalDynamicSecretCount?: number;
totalSecretCount?: number;
totalCount: number;
};
export type DashboardProjectSecretsOverview = Omit<
DashboardProjectSecretsOverviewResponse,
"secrets"
> & {
secrets?: SecretV3RawSanitized[];
};
export type DashboardProjectSecretsDetails = Omit<
DashboardProjectSecretsDetailsResponse,
"secrets"
> & {
secrets?: SecretV3RawSanitized[];
};
export enum DashboardSecretsOrderBy {
Name = "name"
}
export type TGetDashboardProjectSecretsOverviewDTO = {
projectId: string;
secretPath: string;
offset?: number;
limit?: number;
orderBy?: DashboardSecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
includeSecrets?: boolean;
includeFolders?: boolean;
includeDynamicSecrets?: boolean;
environments: string[];
};
export type TGetDashboardProjectSecretsDetailsDTO = Omit<
TGetDashboardProjectSecretsOverviewDTO,
"environments"
> & {
environment: string;
includeImports?: boolean;
tags: Record<string, boolean>;
};

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