diff --git a/backend/package-lock.json b/backend/package-lock.json index 3cd03cd6e..be6137424 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -104,6 +104,7 @@ "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", + "re2": "^1.21.4", "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", @@ -6860,6 +6861,79 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@octokit/auth-app": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz", @@ -11863,6 +11937,115 @@ "node": ">=8" } }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -12842,6 +13025,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -12874,6 +13067,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -13644,6 +13852,12 @@ "node": ">=0.10.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0" + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -15221,6 +15435,12 @@ ], "license": "MIT" }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -15403,7 +15623,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -15436,6 +15655,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/install-artifact-from-github": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.5.tgz", + "integrity": "sha512-gZHC7f/cJgXz7MXlHFBxPVMsvIbev1OQN1uKQYKVJDydGNm9oYf9JstbU4Atnh/eSvk41WtEovoRm+8IF686xg==", + "license": "BSD-3-Clause", + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, "node_modules/internal-slot": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", @@ -15518,6 +15747,19 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ip-num": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ip-num/-/ip-num-1.5.1.tgz", @@ -15717,6 +15959,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -16860,6 +17108,38 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -17033,6 +17313,125 @@ "node": ">=8" } }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", @@ -17354,6 +17753,12 @@ "node": ">=12" } }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -17444,6 +17849,30 @@ } } }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/node-gyp-build": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz", @@ -17465,6 +17894,122 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -18125,6 +18670,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-queue": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", @@ -19202,6 +19762,15 @@ "real-require": "^0.2.0" } }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -19230,6 +19799,28 @@ "node": ">=0.4.0" } }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/prompt-sync": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", @@ -19501,6 +20092,18 @@ "node": ">=0.10.0" } }, + "node_modules/re2": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.21.4.tgz", + "integrity": "sha512-MVIfXWJmsP28mRsSt8HeL750ifb8H5+oF2UDIxGaiJCr8fkMqhLZ7kcX9ADRk2dC8qeGKedB7UVYRfBVpEiLfA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "install-artifact-from-github": "^1.3.5", + "nan": "^2.20.0", + "node-gyp": "^10.2.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -20422,6 +21025,16 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/smee-client": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/smee-client/-/smee-client-2.0.0.tgz", @@ -20625,6 +21238,60 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sonic-boom": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", @@ -20680,6 +21347,27 @@ "node": ">= 0.6" } }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -22482,6 +23170,30 @@ "node": ">=4" } }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/universal-github-app-jwt": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index 66eddcc10..b2c0d751a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -221,6 +221,7 @@ "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", + "re2": "^1.21.4", "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", diff --git a/backend/src/db/migrations/20250421165221_fix-identites-and-user-deletion-secret-version-reference.ts b/backend/src/db/migrations/20250421165221_fix-identites-and-user-deletion-secret-version-reference.ts new file mode 100644 index 000000000..f5280c979 --- /dev/null +++ b/backend/src/db/migrations/20250421165221_fix-identites-and-user-deletion-secret-version-reference.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.dropForeign(["userActorId"]); + table.dropForeign(["identityActorId"]); + }); + + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.foreign("userActorId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + table.foreign("identityActorId").references("id").inTable(TableName.Identity).onDelete("SET NULL"); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.dropForeign(["userActorId"]); + table.dropForeign(["identityActorId"]); + }); + + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.foreign("userActorId").references("id").inTable(TableName.Users); + + table.foreign("identityActorId").references("id").inTable(TableName.Identity); + }); +} diff --git a/backend/src/db/migrations/20250425163216_ssh-nullable-ca-defaults.ts b/backend/src/db/migrations/20250425163216_ssh-nullable-ca-defaults.ts new file mode 100644 index 000000000..2a0b85e1c --- /dev/null +++ b/backend/src/db/migrations/20250425163216_ssh-nullable-ca-defaults.ts @@ -0,0 +1,47 @@ +import { Knex } from "knex"; + +import { ProjectType, TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasDefaultUserCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultUserSshCaId"); + const hasDefaultHostCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultHostSshCaId"); + + if (hasDefaultUserCaCol && hasDefaultHostCaCol) { + await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => { + t.dropForeign(["defaultUserSshCaId"]); + t.dropForeign(["defaultHostSshCaId"]); + }); + await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => { + // allow nullable (does not wipe existing values) + t.uuid("defaultUserSshCaId").nullable().alter(); + t.uuid("defaultHostSshCaId").nullable().alter(); + // re-add with SET NULL behavior (previously CASCADE) + t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL"); + t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL"); + }); + } + + // (dangtony98): backfill by adding null defaults CAs for all existing Infisical SSH projects + // that do not have an associated ProjectSshConfig record introduced in Infisical SSH V2. + + const allProjects = await knex(TableName.Project).where("type", ProjectType.SSH).select("id"); + + const projectsWithConfig = await knex(TableName.ProjectSshConfig).select("projectId"); + const projectIdsWithConfig = new Set(projectsWithConfig.map((config) => config.projectId)); + + const projectsNeedingConfig = allProjects.filter((project) => !projectIdsWithConfig.has(project.id)); + + if (projectsNeedingConfig.length > 0) { + const configsToInsert = projectsNeedingConfig.map((project) => ({ + projectId: project.id, + defaultUserSshCaId: null, + defaultHostSshCaId: null, + createdAt: new Date(), + updatedAt: new Date() + })); + + await knex.batchInsert(TableName.ProjectSshConfig, configsToInsert); + } +} + +export async function down(): Promise {} diff --git a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts index b75a2dae0..7c42c7f99 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { DynamicSecretLeasesSchema } from "@app/db/schemas"; -import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; +import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; import { daysToMillisecond } from "@app/lib/dates"; import { removeTrailingSlash } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; @@ -18,6 +18,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], body: z.object({ dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(), projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug), @@ -65,6 +67,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId) }), @@ -107,6 +111,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId) }), @@ -160,6 +166,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId) }), diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index fdaaf5932..6e70effe4 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models"; -import { DYNAMIC_SECRETS } from "@app/lib/api-docs"; +import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs"; import { daysToMillisecond } from "@app/lib/dates"; import { removeTrailingSlash } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; @@ -21,6 +21,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], body: z.object({ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug), provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider), @@ -111,6 +113,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name) }), @@ -179,6 +183,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name) }), @@ -215,6 +221,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name) }), @@ -253,6 +261,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], querystring: z.object({ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.LIST.path), @@ -284,18 +294,20 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ - name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name) + name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.name) }), querystring: z.object({ - projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.projectSlug), + projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.projectSlug), path: z .string() .trim() .default("/") .transform(removeTrailingSlash) - .describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.path), - environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.environmentSlug) + .describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.path), + environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.environmentSlug) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index 67f955ecb..d10e1800b 100644 --- a/backend/src/ee/routes/v1/group-router.ts +++ b/backend/src/ee/routes/v1/group-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; -import { GROUPS } from "@app/lib/api-docs"; +import { ApiDocsTags, GROUPS } from "@app/lib/api-docs"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,6 +13,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "POST", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], body: z.object({ name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name), slug: slugSchema({ min: 5, max: 36 }).optional().describe(GROUPS.CREATE.slug), @@ -40,6 +42,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "GET", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.GET_BY_ID.id) }), @@ -65,6 +69,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "GET", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], response: { 200: GroupsSchema.array() } @@ -87,6 +93,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "PATCH", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.UPDATE.id) }), @@ -120,6 +128,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "DELETE", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.DELETE.id) }), @@ -145,6 +155,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { url: "/:id/users", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.LIST_USERS.id) }), @@ -194,6 +206,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { url: "/:id/users/:username", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.ADD_USER.id), username: z.string().trim().describe(GROUPS.ADD_USER.username) @@ -227,6 +241,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { url: "/:id/users/:username", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.DELETE_USER.id), username: z.string().trim().describe(GROUPS.DELETE_USER.username) diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index d6b1a4c5c..f64d3c979 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; -import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; +import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { UnauthorizedError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -25,6 +25,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Create a permanent or a non expiry specific privilege for identity.", security: [ { @@ -85,6 +87,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Create a temporary or a expiring specific privilege for identity.", security: [ { @@ -157,6 +161,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Update a specific privilege of an identity.", security: [ { @@ -240,6 +246,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Delete a specific privilege of an identity.", security: [ { @@ -279,6 +287,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Retrieve details of a specific privilege by privilege slug.", security: [ { @@ -319,6 +329,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "List of a specific privilege of an identity in a project.", security: [ { diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index e3956731e..ab9d1be6c 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -17,6 +17,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Return project secret snapshots ids", security: [ { diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index cabf65337..08d16414b 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -5,7 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; -import { ProjectTemplates } from "@app/lib/api-docs"; +import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -101,6 +101,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "List project templates for the current organization.", response: { 200: z.object({ @@ -137,6 +139,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Get a project template by ID.", params: z.object({ templateId: z.string().uuid() @@ -176,6 +180,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Create a project template.", body: z.object({ name: slugSchema({ field: "name" }) @@ -219,6 +225,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Update a project template.", params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }), body: z.object({ @@ -269,6 +277,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Delete a project template.", params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }), diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 494871ec1..3ee80adce 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { SecretSnapshotsSchema } from "@app/db/schemas"; -import { PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; @@ -65,6 +65,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Roll back project secrets to those captured in a secret snapshot version.", security: [ { diff --git a/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts b/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts index 783cb9b72..e20d49263 100644 --- a/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts +++ b/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts @@ -6,7 +6,7 @@ import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-s import { SshCaKeySource, SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types"; import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; -import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -20,6 +20,8 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], description: "Create SSH CA", body: z .object({ @@ -92,6 +94,8 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], description: "Get SSH CA", params: z.object({ sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET.sshCaId) @@ -138,6 +142,8 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], description: "Get public key of SSH CA", params: z.object({ sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_PUBLIC_KEY.sshCaId) @@ -163,6 +169,8 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], description: "Update SSH CA", params: z.object({ sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId) @@ -216,6 +224,8 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], description: "Delete SSH CA", params: z.object({ sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.DELETE.sshCaId) @@ -261,6 +271,8 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], description: "Get list of certificate templates for the SSH CA", params: z.object({ sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_CERTIFICATE_TEMPLATES.sshCaId) diff --git a/backend/src/ee/routes/v1/ssh-certificate-router.ts b/backend/src/ee/routes/v1/ssh-certificate-router.ts index eb0fc158a..cb576e496 100644 --- a/backend/src/ee/routes/v1/ssh-certificate-router.ts +++ b/backend/src/ee/routes/v1/ssh-certificate-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; -import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -20,6 +20,8 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SshCertificates], description: "Sign SSH public key", body: z.object({ certificateTemplateId: z @@ -100,6 +102,8 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SshCertificates], description: "Issue SSH credentials (certificate + key)", body: z.object({ certificateTemplateId: z diff --git a/backend/src/ee/routes/v1/ssh-certificate-template-router.ts b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts index a7dc55661..e44693643 100644 --- a/backend/src/ee/routes/v1/ssh-certificate-template-router.ts +++ b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts @@ -8,7 +8,7 @@ import { isValidHostPattern, isValidUserPattern } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators"; -import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; +import { ApiDocsTags, SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -22,6 +22,8 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], params: z.object({ certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.GET.certificateTemplateId) }), @@ -61,6 +63,8 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], body: z .object({ sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId), @@ -141,6 +145,8 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], body: z.object({ status: z.nativeEnum(SshCertTemplateStatus).optional(), name: z @@ -224,6 +230,8 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], params: z.object({ certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId) }), diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index c8f0ba16c..a6d4459e4 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; +import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -21,6 +21,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Add an additional privilege for identity.", security: [ { @@ -84,6 +86,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Update a specific identity privilege.", security: [ { @@ -148,6 +152,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Delete the specified identity privilege.", security: [ { @@ -183,6 +189,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Retrieve details of a specific privilege by id.", security: [ { @@ -218,6 +226,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Retrieve details of a specific privilege by slug.", security: [ { @@ -258,6 +268,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "List privileges for the specified identity by project.", security: [ { diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts index 0bb83b8d4..538929316 100644 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ b/backend/src/ee/routes/v2/project-role-router.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { PROJECT_ROLE } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -20,6 +20,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Create a project role", security: [ { @@ -75,6 +77,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Update a project role", security: [ { @@ -130,6 +134,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Delete a project role", security: [ { @@ -166,6 +172,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "List project role", security: [ { @@ -204,6 +212,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], params: z.object({ projectId: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectId), roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug) diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/aws-iam-user-secret-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/aws-iam-user-secret-rotation-router.ts new file mode 100644 index 000000000..489f3ea55 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/aws-iam-user-secret-rotation-router.ts @@ -0,0 +1,19 @@ +import { + AwsIamUserSecretRotationGeneratedCredentialsSchema, + AwsIamUserSecretRotationSchema, + CreateAwsIamUserSecretRotationSchema, + UpdateAwsIamUserSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerAwsIamUserSecretRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.AwsIamUserSecret, + server, + responseSchema: AwsIamUserSecretRotationSchema, + createSchema: CreateAwsIamUserSecretRotationSchema, + updateSchema: UpdateAwsIamUserSecretRotationSchema, + generatedCredentialsSchema: AwsIamUserSecretRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 1dacf1bd2..3dcdac30b 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -1,6 +1,8 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router"; +import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router"; +import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; @@ -12,5 +14,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< > = { [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter, - [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter + [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter, + [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter, + [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts new file mode 100644 index 000000000..04d2b50ac --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreateLdapPasswordRotationSchema, + LdapPasswordRotationGeneratedCredentialsSchema, + LdapPasswordRotationSchema, + UpdateLdapPasswordRotationSchema +} from "@app/ee/services/secret-rotation-v2/ldap-password"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.LdapPassword, + server, + responseSchema: LdapPasswordRotationSchema, + createSchema: CreateLdapPasswordRotationSchema, + updateSchema: UpdateLdapPasswordRotationSchema, + generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts index 05d3c7961..17fe14dbf 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts @@ -9,7 +9,7 @@ import { TSecretRotationV2GeneratedCredentials, TSecretRotationV2Input } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; -import { SecretRotations } from "@app/lib/api-docs"; +import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs"; import { startsWithVowel } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -66,6 +66,8 @@ export const registerSecretRotationEndpoints = < rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `List the ${rotationType} Rotations for the specified project.`, querystring: z.object({ projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST(type).projectId) @@ -109,6 +111,8 @@ export const registerSecretRotationEndpoints = < rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `Get the specified ${rotationType} Rotation by ID.`, params: z.object({ rotationId: z.string().uuid().describe(SecretRotations.GET_BY_ID(type).rotationId) @@ -151,6 +155,8 @@ export const registerSecretRotationEndpoints = < rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `Get the specified ${rotationType} Rotation by name, secret path, environment and project ID.`, params: z.object({ rotationName: z @@ -215,6 +221,8 @@ export const registerSecretRotationEndpoints = < rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `Create ${ startsWithVowel(rotationType) ? "an" : "a" } ${rotationType} Rotation for the specified project.`, @@ -254,6 +262,8 @@ export const registerSecretRotationEndpoints = < rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `Update the specified ${rotationType} Rotation.`, params: z.object({ rotationId: z.string().uuid().describe(SecretRotations.UPDATE(type).rotationId) @@ -296,6 +306,8 @@ export const registerSecretRotationEndpoints = < rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `Delete the specified ${rotationType} Rotation.`, params: z.object({ rotationId: z.string().uuid().describe(SecretRotations.DELETE(type).rotationId) @@ -349,6 +361,8 @@ export const registerSecretRotationEndpoints = < rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `Get the generated credentials for the specified ${rotationType} Rotation.`, params: z.object({ rotationId: z.string().uuid().describe(SecretRotations.GET_GENERATED_CREDENTIALS_BY_ID(type).rotationId) @@ -402,6 +416,8 @@ export const registerSecretRotationEndpoints = < rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: `Rotate the generated credentials for the specified ${rotationType} Rotation.`, params: z.object({ rotationId: z.string().uuid().describe(SecretRotations.ROTATE(type).rotationId) diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index bfb8b38c0..772f70035 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -2,10 +2,12 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret"; +import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; -import { SecretRotations } from "@app/lib/api-docs"; +import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,7 +15,9 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ PostgresCredentialsRotationListItemSchema, MsSqlCredentialsRotationListItemSchema, - Auth0ClientSecretRotationListItemSchema + Auth0ClientSecretRotationListItemSchema, + LdapPasswordRotationListItemSchema, + AwsIamUserSecretRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { @@ -24,6 +28,8 @@ export const registerSecretRotationV2Router = async (server: FastifyZodProvider) rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: "List the available Secret Rotation Options.", response: { 200: z.object({ @@ -45,6 +51,8 @@ export const registerSecretRotationV2Router = async (server: FastifyZodProvider) rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], description: "List all the Secret Rotations for the specified project.", querystring: z.object({ projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST().projectId) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b10e449b0..67449547b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -234,6 +234,7 @@ export enum EventType { GET_PROJECT_KMS_BACKUP = "get-project-kms-backup", LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup", ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project", + ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", @@ -248,6 +249,8 @@ export enum EventType { DELETE_SLACK_INTEGRATION = "delete-slack-integration", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", + GET_PROJECT_SSH_CONFIG = "get-project-ssh-config", + UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config", INTEGRATION_SYNCED = "integration-synced", CREATE_CMEK = "create-cmek", UPDATE_CMEK = "update-cmek", @@ -1909,6 +1912,11 @@ interface OrgAdminAccessProjectEvent { }; // no metadata yet } +interface OrgAdminBypassSSOEvent { + type: EventType.ORG_ADMIN_BYPASS_SSO; + metadata: Record; // no metadata yet +} + interface CreateCertificateTemplateEstConfig { type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG; metadata: { @@ -1988,6 +1996,25 @@ interface GetProjectSlackConfig { id: string; }; } + +interface GetProjectSshConfig { + type: EventType.GET_PROJECT_SSH_CONFIG; + metadata: { + id: string; + projectId: string; + }; +} + +interface UpdateProjectSshConfig { + type: EventType.UPDATE_PROJECT_SSH_CONFIG; + metadata: { + id: string; + projectId: string; + defaultUserSshCaId?: string | null; + defaultHostSshCaId?: string | null; + }; +} + interface IntegrationSyncedEvent { type: EventType.INTEGRATION_SYNCED; metadata: { @@ -2681,6 +2708,7 @@ export type Event = | GetProjectKmsBackupEvent | LoadProjectKmsBackupEvent | OrgAdminAccessProjectEvent + | OrgAdminBypassSSOEvent | CreateCertificateTemplate | UpdateCertificateTemplate | GetCertificateTemplate @@ -2695,6 +2723,8 @@ export type Event = | GetSlackIntegration | UpdateProjectSlackConfig | GetProjectSlackConfig + | GetProjectSshConfig + | UpdateProjectSshConfig | IntegrationSyncedEvent | CreateCmekEvent | UpdateCmekEvent diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 88f2d90f1..4adf8b7e2 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -130,7 +130,17 @@ export const dynamicSecretLeaseServiceFactory = ({ if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" }); } - const { entityId, data } = await selectedProvider.create(decryptedStoredInput, expireAt.getTime()); + let result; + try { + result = await selectedProvider.create(decryptedStoredInput, expireAt.getTime()); + } catch (error: unknown) { + if (error && typeof error === "object" && error !== null && "sqlMessage" in error) { + throw new BadRequestError({ message: error.sqlMessage as string }); + } + throw error; + } + const { entityId, data } = result; + const dynamicSecretLease = await dynamicSecretLeaseDAL.create({ expireAt, version: 1, diff --git a/backend/src/ee/services/dynamic-secret/providers/ldap.ts b/backend/src/ee/services/dynamic-secret/providers/ldap.ts index 992c9098e..cc68304e0 100644 --- a/backend/src/ee/services/dynamic-secret/providers/ldap.ts +++ b/backend/src/ee/services/dynamic-secret/providers/ldap.ts @@ -2,6 +2,7 @@ import handlebars from "handlebars"; import ldapjs from "ldapjs"; import ldif from "ldif"; import { customAlphabet } from "nanoid"; +import RE2 from "re2"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; @@ -194,7 +195,8 @@ export const LdapProvider = (): TDynamicProviderFns => { const client = await $getClient(providerInputs); if (providerInputs.credentialType === LdapCredentialType.Static) { - const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m); + const dnRegex = new RE2("^dn:\\s*(.+)", "m"); + const dnMatch = dnRegex.exec(providerInputs.rotationLdif); if (dnMatch) { const username = dnMatch[1]; @@ -238,7 +240,8 @@ export const LdapProvider = (): TDynamicProviderFns => { const client = await $getClient(providerInputs); if (providerInputs.credentialType === LdapCredentialType.Static) { - const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m); + const dnRegex = new RE2("^dn:\\s*(.+)", "m"); + const dnMatch = dnRegex.exec(providerInputs.rotationLdif); if (dnMatch) { const username = dnMatch[1]; diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index 49ac293ed..4d7b1a5b5 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -83,18 +83,26 @@ export const externalKmsServiceFactory = ({ throw error; }); - // if missing kms key this generate a new kms key id and returns new provider input - const newProviderInput = await externalKms.generateInputKmsKey(); - sanitizedProviderInput = JSON.stringify(newProviderInput); + try { + // if missing kms key this generate a new kms key id and returns new provider input + const newProviderInput = await externalKms.generateInputKmsKey(); + sanitizedProviderInput = JSON.stringify(newProviderInput); - await externalKms.validateConnection(); + await externalKms.validateConnection(); + } finally { + await externalKms.cleanup(); + } } break; case KmsProviders.Gcp: { const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs }); - await externalKms.validateConnection(); - sanitizedProviderInput = JSON.stringify(provider.inputs); + try { + await externalKms.validateConnection(); + sanitizedProviderInput = JSON.stringify(provider.inputs); + } finally { + await externalKms.cleanup(); + } } break; default: @@ -186,8 +194,12 @@ export const externalKmsServiceFactory = ({ ); const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs }; const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput }); - await externalKms.validateConnection(); - sanitizedProviderInput = JSON.stringify(updatedProviderInput); + try { + await externalKms.validateConnection(); + sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } finally { + await externalKms.cleanup(); + } } break; case KmsProviders.Gcp: @@ -197,8 +209,12 @@ export const externalKmsServiceFactory = ({ ); const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs }; const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput }); - await externalKms.validateConnection(); - sanitizedProviderInput = JSON.stringify(updatedProviderInput); + try { + await externalKms.validateConnection(); + sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } finally { + await externalKms.cleanup(); + } } break; default: @@ -368,7 +384,11 @@ export const externalKmsServiceFactory = ({ const fetchGcpKeys = async ({ credential, gcpRegion }: Pick) => { const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } }); - return externalKms.getKeysList(); + try { + return await externalKms.getKeysList(); + } finally { + await externalKms.cleanup(); + } }; return { diff --git a/backend/src/ee/services/external-kms/providers/aws-kms.ts b/backend/src/ee/services/external-kms/providers/aws-kms.ts index 6d9166a3a..2bda9c75e 100644 --- a/backend/src/ee/services/external-kms/providers/aws-kms.ts +++ b/backend/src/ee/services/external-kms/providers/aws-kms.ts @@ -102,10 +102,19 @@ export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Pro return { data: Buffer.from(decryptionCommand.Plaintext) }; }; + const cleanup = async () => { + try { + awsClient.destroy(); + } catch (error) { + throw new Error("Failed to cleanup AWS KMS client", { cause: error }); + } + }; + return { generateInputKmsKey, validateConnection, encrypt, - decrypt + decrypt, + cleanup }; }; diff --git a/backend/src/ee/services/external-kms/providers/gcp-kms.ts b/backend/src/ee/services/external-kms/providers/gcp-kms.ts index bee1eb24b..ff2820fe8 100644 --- a/backend/src/ee/services/external-kms/providers/gcp-kms.ts +++ b/backend/src/ee/services/external-kms/providers/gcp-kms.ts @@ -45,6 +45,14 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro } }; + const cleanup = async () => { + try { + await gcpKmsClient.close(); + } catch (error) { + throw new Error("Failed to cleanup GCP KMS client", { cause: error }); + } + }; + // Used when adding the KMS to fetch the list of keys in specified region const getKeysList = async () => { try { @@ -108,6 +116,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro validateConnection, getKeysList, encrypt, - decrypt + decrypt, + cleanup }; }; diff --git a/backend/src/ee/services/external-kms/providers/model.ts b/backend/src/ee/services/external-kms/providers/model.ts index 436b39423..6cb78a34e 100644 --- a/backend/src/ee/services/external-kms/providers/model.ts +++ b/backend/src/ee/services/external-kms/providers/model.ts @@ -98,4 +98,5 @@ export type TExternalKmsProviderFns = { validateConnection: () => Promise; encrypt: (data: Buffer) => Promise<{ encryptedBlob: Buffer }>; decrypt: (encryptedBlob: Buffer) => Promise<{ data: Buffer }>; + cleanup: () => Promise; }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 41a1bf5ba..8e6645073 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -969,7 +969,6 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); - can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); @@ -1035,7 +1034,6 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 480e80028..90fdf561e 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -267,7 +267,6 @@ export const secretReplicationServiceFactory = ({ const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared }); const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id }); const sourceImportedSecrets = await fnSecretsV2FromImports({ - projectId, secretImports: sourceSecretImports, secretDAL: secretV2BridgeDAL, folderDAL, diff --git a/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-constants.ts new file mode 100644 index 000000000..1b36b5f30 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "AWS IAM User Secret", + type: SecretRotation.AwsIamUserSecret, + connection: AppConnection.AWS, + template: { + secretsMapping: { + accessKeyId: "AWS_ACCESS_KEY_ID", + secretAccessKey: "AWS_SECRET_ACCESS_KEY" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-fns.ts new file mode 100644 index 000000000..c08eb162b --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-fns.ts @@ -0,0 +1,123 @@ +import AWS from "aws-sdk"; + +import { + TAwsIamUserSecretRotationGeneratedCredentials, + TAwsIamUserSecretRotationWithConnection +} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types"; +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws"; + +const getCreateDate = (key: AWS.IAM.AccessKeyMetadata): number => { + return key.CreateDate ? new Date(key.CreateDate).getTime() : 0; +}; + +export const awsIamUserSecretRotationFactory: TRotationFactory< + TAwsIamUserSecretRotationWithConnection, + TAwsIamUserSecretRotationGeneratedCredentials +> = (secretRotation) => { + const { + parameters: { region, userName }, + connection, + secretsMapping + } = secretRotation; + + const $rotateClientSecret = async () => { + const { credentials } = await getAwsConnectionConfig(connection, region); + const iam = new AWS.IAM({ credentials }); + + const { AccessKeyMetadata } = await iam.listAccessKeys({ UserName: userName }).promise(); + + if (AccessKeyMetadata && AccessKeyMetadata.length > 0) { + // Sort keys by creation date (oldest first) + const sortedKeys = [...AccessKeyMetadata].sort((a, b) => getCreateDate(a) - getCreateDate(b)); + + // If we already have 2 keys, delete the oldest one + if (sortedKeys.length >= 2) { + const accessId = sortedKeys[0].AccessKeyId || sortedKeys[1].AccessKeyId; + if (accessId) { + await iam + .deleteAccessKey({ + UserName: userName, + AccessKeyId: accessId + }) + .promise(); + } + } + } + + const { AccessKey } = await iam.createAccessKey({ UserName: userName }).promise(); + + return { + accessKeyId: AccessKey.AccessKeyId, + secretAccessKey: AccessKey.SecretAccessKey + }; + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotateClientSecret(); + + return callback(credentials); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + generatedCredentials, + callback + ) => { + const { credentials } = await getAwsConnectionConfig(connection, region); + const iam = new AWS.IAM({ credentials }); + + await Promise.all( + generatedCredentials.map((generatedCredential) => + iam + .deleteAccessKey({ + UserName: userName, + AccessKeyId: generatedCredential.accessKeyId + }) + .promise() + ) + ); + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = await $rotateClientSecret(); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const secrets = [ + { + key: secretsMapping.accessKeyId, + value: generatedCredentials.accessKeyId + }, + { + key: secretsMapping.secretAccessKey, + value: generatedCredentials.secretAccessKey + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-schemas.ts new file mode 100644 index 000000000..dba4c6102 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-schemas.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; + +export const AwsIamUserSecretRotationGeneratedCredentialsSchema = z + .object({ + accessKeyId: z.string(), + secretAccessKey: z.string() + }) + .array() + .min(1) + .max(2); + +const AwsIamUserSecretRotationParametersSchema = z.object({ + userName: z + .string() + .trim() + .min(1, "Client Name Required") + .describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.userName), + region: z.nativeEnum(AWSRegion).describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.region).optional() +}); + +const AwsIamUserSecretRotationSecretsMappingSchema = z.object({ + accessKeyId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.accessKeyId), + secretAccessKey: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.secretAccessKey) +}); + +export const AwsIamUserSecretRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + accessKeyId: z.string(), + secretAccessKey: z.string() + }) +}); + +export const AwsIamUserSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AwsIamUserSecret).extend({ + type: z.literal(SecretRotation.AwsIamUserSecret), + parameters: AwsIamUserSecretRotationParametersSchema, + secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema +}); + +export const CreateAwsIamUserSecretRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.AwsIamUserSecret +).extend({ + parameters: AwsIamUserSecretRotationParametersSchema, + secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema +}); + +export const UpdateAwsIamUserSecretRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.AwsIamUserSecret +).extend({ + parameters: AwsIamUserSecretRotationParametersSchema.optional(), + secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema.optional() +}); + +export const AwsIamUserSecretRotationListItemSchema = z.object({ + name: z.literal("AWS IAM User Secret"), + connection: z.literal(AppConnection.AWS), + type: z.literal(SecretRotation.AwsIamUserSecret), + template: AwsIamUserSecretRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types.ts new file mode 100644 index 000000000..5db7ef2b0 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws"; + +import { + AwsIamUserSecretRotationGeneratedCredentialsSchema, + AwsIamUserSecretRotationListItemSchema, + AwsIamUserSecretRotationSchema, + CreateAwsIamUserSecretRotationSchema +} from "./aws-iam-user-secret-rotation-schemas"; + +export type TAwsIamUserSecretRotation = z.infer; + +export type TAwsIamUserSecretRotationInput = z.infer; + +export type TAwsIamUserSecretRotationListItem = z.infer; + +export type TAwsIamUserSecretRotationWithConnection = TAwsIamUserSecretRotation & { + connection: TAwsConnection; +}; + +export type TAwsIamUserSecretRotationGeneratedCredentials = z.infer< + typeof AwsIamUserSecretRotationGeneratedCredentialsSchema +>; diff --git a/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/index.ts b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/index.ts new file mode 100644 index 000000000..69635c68c --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/aws-iam-user-secret/index.ts @@ -0,0 +1,3 @@ +export * from "./aws-iam-user-secret-rotation-constants"; +export * from "./aws-iam-user-secret-rotation-schemas"; +export * from "./aws-iam-user-secret-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts new file mode 100644 index 000000000..55929169a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts @@ -0,0 +1,3 @@ +export * from "./ldap-password-rotation-constants"; +export * from "./ldap-password-rotation-schemas"; +export * from "./ldap-password-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts new file mode 100644 index 000000000..061bf11ea --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "LDAP Password", + type: SecretRotation.LdapPassword, + connection: AppConnection.LDAP, + template: { + secretsMapping: { + dn: "LDAP_DN", + password: "LDAP_PASSWORD" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts new file mode 100644 index 000000000..0fd01b753 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts @@ -0,0 +1,181 @@ +import ldap from "ldapjs"; + +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { logger } from "@app/lib/logger"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap"; + +import { generatePassword } from "../shared/utils"; +import { + TLdapPasswordRotationGeneratedCredentials, + TLdapPasswordRotationWithConnection +} from "./ldap-password-rotation-types"; + +const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le"); + +export const ldapPasswordRotationFactory: TRotationFactory< + TLdapPasswordRotationWithConnection, + TLdapPasswordRotationGeneratedCredentials +> = (secretRotation, appConnectionDAL, kmsService) => { + const { + connection, + parameters: { dn, passwordRequirements }, + secretsMapping + } = secretRotation; + + const $verifyCredentials = async (credentials: Pick) => { + try { + const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials }); + + client.unbind(); + client.destroy(); + } catch (error) { + throw new Error(`Failed to verify credentials - ${(error as Error).message}`); + } + }; + + const $rotatePassword = async () => { + const { credentials, orgId } = connection; + + if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection"); + + const client = await getLdapConnectionClient(credentials); + const isPersonalRotation = credentials.dn === dn; + + const password = generatePassword(passwordRequirements); + + let changes: ldap.Change[] | ldap.Change; + + switch (credentials.provider) { + case LdapProvider.ActiveDirectory: + { + const encodedPassword = getEncodedPassword(password); + + // service account vs personal password rotation require different changes + if (isPersonalRotation) { + const currentEncodedPassword = getEncodedPassword(credentials.password); + + changes = [ + new ldap.Change({ + operation: "delete", + modification: { + type: "unicodePwd", + values: [currentEncodedPassword] + } + }), + new ldap.Change({ + operation: "add", + modification: { + type: "unicodePwd", + values: [encodedPassword] + } + }) + ]; + } else { + changes = new ldap.Change({ + operation: "replace", + modification: { + type: "unicodePwd", + values: [encodedPassword] + } + }); + } + } + break; + default: + throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`); + } + + try { + await new Promise((resolve, reject) => { + client.modify(dn, changes, (err) => { + if (err) { + logger.error(err, "LDAP Password Rotation Failed"); + reject(new Error(`Provider Modify Error: ${err.message}`)); + } else { + resolve(true); + } + }); + }); + } finally { + client.unbind(); + client.destroy(); + } + + await $verifyCredentials({ dn, password }); + + if (isPersonalRotation) { + const updatedCredentials: TLdapConnection["credentials"] = { + ...credentials, + password + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(connection.id, { encryptedCredentials }); + } + + return { dn, password }; + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotatePassword(); + + return callback(credentials); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + _, + callback + ) => { + // we just rotate to a new password, essentially revoking old credentials + await $rotatePassword(); + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = await $rotatePassword(); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const secrets = [ + { + key: secretsMapping.dn, + value: generatedCredentials.dn + }, + { + key: secretsMapping.password, + value: generatedCredentials.password + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts new file mode 100644 index 000000000..e99569d9a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts @@ -0,0 +1,68 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general"; +import { SecretRotations } from "@app/lib/api-docs"; +import { DistinguishedNameRegex } from "@app/lib/regex"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const LdapPasswordRotationGeneratedCredentialsSchema = z + .object({ + dn: z.string(), + password: z.string() + }) + .array() + .min(1) + .max(2); + +const LdapPasswordRotationParametersSchema = z.object({ + dn: z + .string() + .trim() + .regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") + .min(1, "Distinguished Name (DN) Required") + .describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn), + passwordRequirements: PasswordRequirementsSchema.optional() +}); + +const LdapPasswordRotationSecretsMappingSchema = z.object({ + dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn), + password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password) +}); + +export const LdapPasswordRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + dn: z.string(), + password: z.string() + }) +}); + +export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({ + type: z.literal(SecretRotation.LdapPassword), + parameters: LdapPasswordRotationParametersSchema, + secretsMapping: LdapPasswordRotationSecretsMappingSchema +}); + +export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({ + parameters: LdapPasswordRotationParametersSchema, + secretsMapping: LdapPasswordRotationSecretsMappingSchema +}); + +export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({ + parameters: LdapPasswordRotationParametersSchema.optional(), + secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional() +}); + +export const LdapPasswordRotationListItemSchema = z.object({ + name: z.literal("LDAP Password"), + connection: z.literal(AppConnection.LDAP), + type: z.literal(SecretRotation.LdapPassword), + template: LdapPasswordRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts new file mode 100644 index 000000000..cb15b0734 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { TLdapConnection } from "@app/services/app-connection/ldap"; + +import { + CreateLdapPasswordRotationSchema, + LdapPasswordRotationGeneratedCredentialsSchema, + LdapPasswordRotationListItemSchema, + LdapPasswordRotationSchema +} from "./ldap-password-rotation-schemas"; + +export type TLdapPasswordRotation = z.infer; + +export type TLdapPasswordRotationInput = z.infer; + +export type TLdapPasswordRotationListItem = z.infer; + +export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & { + connection: TLdapConnection; +}; + +export type TLdapPasswordRotationGeneratedCredentials = z.infer; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index d43cacb3a..4ddf4ee0c 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -1,7 +1,9 @@ export enum SecretRotation { PostgresCredentials = "postgres-credentials", MsSqlCredentials = "mssql-credentials", - Auth0ClientSecret = "auth0-client-secret" + Auth0ClientSecret = "auth0-client-secret", + LdapPassword = "ldap-password", + AwsIamUserSecret = "aws-iam-user-secret" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 603b77cc1..23452d7d4 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -4,6 +4,8 @@ import { getConfig } from "@app/lib/config/env"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; +import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret"; +import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; @@ -18,7 +20,9 @@ import { const SECRET_ROTATION_LIST_OPTIONS: Record = { [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION, - [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION + [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION, + [SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION, + [SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION }; export const listSecretRotationOptions = () => { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 1050c3419..134aaeafc 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -3,12 +3,16 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", - [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials", - [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret" + [SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials", + [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", + [SecretRotation.LdapPassword]: "LDAP Password", + [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, - [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, + [SecretRotation.LdapPassword]: AppConnection.LDAP, + [SecretRotation.AwsIamUserSecret]: AppConnection.AWS }; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index a828acb32..70543c9b4 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -14,6 +14,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns"; +import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns"; import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { calculateNextRotationAt, @@ -77,6 +78,7 @@ import { import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; +import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; export type TSecretRotationV2ServiceFactoryDep = { @@ -114,7 +116,9 @@ type TRotationFactoryImplementation = TRotationFactory< const SECRET_ROTATION_FACTORY_MAP: Record = { [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, - [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation + [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation, + [SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation, + [SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation }; export const secretRotationV2ServiceFactory = ({ @@ -449,6 +453,18 @@ export const secretRotationV2ServiceFactory = ({ kmsService ); + // even though we have a db constraint we want to check before any rotation of credentials is attempted + // to prevent creation failure after external credentials have been modified + const conflictingRotation = await secretRotationV2DAL.findOne({ + name: payload.name, + folderId: folder.id + }); + + if (conflictingRotation) + throw new BadRequestError({ + message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"` + }); + try { const currentTime = new Date(); diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index c52fa5465..bd44d2778 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -12,6 +12,20 @@ import { TAuth0ClientSecretRotationListItem, TAuth0ClientSecretRotationWithConnection } from "./auth0-client-secret"; +import { + TAwsIamUserSecretRotation, + TAwsIamUserSecretRotationGeneratedCredentials, + TAwsIamUserSecretRotationInput, + TAwsIamUserSecretRotationListItem, + TAwsIamUserSecretRotationWithConnection +} from "./aws-iam-user-secret"; +import { + TLdapPasswordRotation, + TLdapPasswordRotationGeneratedCredentials, + TLdapPasswordRotationInput, + TLdapPasswordRotationListItem, + TLdapPasswordRotationWithConnection +} from "./ldap-password"; import { TMsSqlCredentialsRotation, TMsSqlCredentialsRotationInput, @@ -27,26 +41,39 @@ import { import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { SecretRotation } from "./secret-rotation-v2-enums"; -export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation; +export type TSecretRotationV2 = + | TPostgresCredentialsRotation + | TMsSqlCredentialsRotation + | TAuth0ClientSecretRotation + | TLdapPasswordRotation + | TAwsIamUserSecretRotation; export type TSecretRotationV2WithConnection = | TPostgresCredentialsRotationWithConnection | TMsSqlCredentialsRotationWithConnection - | TAuth0ClientSecretRotationWithConnection; + | TAuth0ClientSecretRotationWithConnection + | TLdapPasswordRotationWithConnection + | TAwsIamUserSecretRotationWithConnection; export type TSecretRotationV2GeneratedCredentials = | TSqlCredentialsRotationGeneratedCredentials - | TAuth0ClientSecretRotationGeneratedCredentials; + | TAuth0ClientSecretRotationGeneratedCredentials + | TLdapPasswordRotationGeneratedCredentials + | TAwsIamUserSecretRotationGeneratedCredentials; export type TSecretRotationV2Input = | TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput - | TAuth0ClientSecretRotationInput; + | TAuth0ClientSecretRotationInput + | TLdapPasswordRotationInput + | TAwsIamUserSecretRotationInput; export type TSecretRotationV2ListItem = | TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem - | TAuth0ClientSecretRotationListItem; + | TAuth0ClientSecretRotationListItem + | TLdapPasswordRotationListItem + | TAwsIamUserSecretRotationListItem; export type TSecretRotationV2Raw = NonNullable>>; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts index 2db9c0251..4d51a23c3 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts @@ -1,11 +1,16 @@ import { z } from "zod"; import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; +import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret"; + export const SecretRotationV2Schema = z.discriminatedUnion("type", [ PostgresCredentialsRotationSchema, MsSqlCredentialsRotationSchema, - Auth0ClientSecretRotationSchema + Auth0ClientSecretRotationSchema, + LdapPasswordRotationSchema, + AwsIamUserSecretRotationSchema ]); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts new file mode 100644 index 000000000..9b2148414 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts @@ -0,0 +1 @@ +export * from "./password-requirements-schema"; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts b/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts new file mode 100644 index 000000000..5d575239d --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts @@ -0,0 +1,44 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { SecretRotations } from "@app/lib/api-docs"; + +export const PasswordRequirementsSchema = z + .object({ + length: z + .number() + .min(1, "Password length must be a positive number") + .max(250, "Password length must be less than 250") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length), + required: z.object({ + digits: z + .number() + .min(0, "Digit count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits), + lowercase: z + .number() + .min(0, "Lowercase count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase), + uppercase: z + .number() + .min(0, "Uppercase count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase), + symbols: z + .number() + .min(0, "Symbol count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols) + }), + allowedSymbols: z + .string() + .regex(new RE2("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?~]"), "Invalid symbols") + .optional() + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols) + }) + .refine((data) => { + return Object.values(data.required).some((count) => count > 0); + }, "At least one character type must be required") + .refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, "Sum of required characters cannot exceed the total length") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts index dfe4c22ed..9b2eb7839 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts @@ -1,6 +1,17 @@ import { randomInt } from "crypto"; -const DEFAULT_PASSWORD_REQUIREMENTS = { +type TPasswordRequirements = { + length: number; + required: { + lowercase: number; + uppercase: number; + digits: number; + symbols: number; + }; + allowedSymbols?: string; +}; + +const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = { length: 48, required: { lowercase: 1, @@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = { allowedSymbols: "-_.~!*" }; -export const generatePassword = () => { +export const generatePassword = (passwordRequirements?: TPasswordRequirements) => { try { - const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS; + const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS; const chars = { lowercase: "abcdefghijklmnopqrstuvwxyz", diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts index 48d793970..a09cc55ff 100644 --- a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts @@ -1,4 +1,5 @@ import { isIP } from "net"; +import RE2 from "re2"; import { isFQDN } from "@app/lib/validator/validate-url"; @@ -10,7 +11,7 @@ export const isValidUserPattern = (value: string): boolean => { if (value === "*") return true; // Handle wildcard separately // Simpler, more specific pattern for usernames - const userRegex = /^[a-z_][a-z0-9_-]*$/i; + const userRegex = new RE2(/^[a-z_][a-z0-9_-]*$/i); return userRegex.test(value); }; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts index 92f946747..60c966fcd 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts @@ -4,6 +4,7 @@ import { promises as fs } from "fs"; import { Knex } from "knex"; import os from "os"; import path from "path"; +import RE2 from "re2"; import { promisify } from "util"; import { TSshCertificateTemplates } from "@app/db/schemas"; @@ -156,14 +157,14 @@ export const validateSshCertificatePrincipals = ( }); } - if (/\r|\n|\t|\0/.test(sanitized)) { + if (new RE2(/\r|\n|\t|\0/).test(sanitized)) { throw new BadRequestError({ message: `Principal '${sanitized}' contains invalid whitespace or control characters.` }); } // disallow whitespace anywhere - if (/\s/.test(sanitized)) { + if (new RE2(/\s/).test(sanitized)) { throw new BadRequestError({ message: `Principal '${sanitized}' cannot contain whitespace.` }); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ff07940bc..18157d979 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -8,6 +8,51 @@ import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connec import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +export enum ApiDocsTags { + Identities = "Identities", + TokenAuth = "Token Auth", + UniversalAuth = "Universal Auth", + GcpAuth = "GCP Auth", + AwsAuth = "AWS Auth", + AzureAuth = "Azure Auth", + KubernetesAuth = "Kubernetes Auth", + JwtAuth = "JWT Auth", + OidcAuth = "OIDC Auth", + Groups = "Groups", + Organizations = "Organizations", + Projects = "Projects", + ProjectUsers = "Project Users", + ProjectGroups = "Project Groups", + ProjectIdentities = "Project Identities", + ProjectRoles = "Project Roles", + ProjectTemplates = "Project Templates", + Environments = "Environments", + Folders = "Folders", + SecretTags = "Secret Tags", + Secrets = "Secrets", + DynamicSecrets = "Dynamic Secrets", + SecretImports = "Secret Imports", + SecretRotations = "Secret Rotations", + IdentitySpecificPrivilegesV1 = "Identity Specific Privileges", + IdentitySpecificPrivilegesV2 = "Identity Specific Privileges V2", + AppConnections = "App Connections", + SecretSyncs = "Secret Syncs", + Integrations = "Integrations", + ServiceTokens = "Service Tokens", + AuditLogs = "Audit Logs", + PkiCertificateAuthorities = "PKI Certificate Authorities", + PkiCertificates = "PKI Certificates", + PkiCertificateTemplates = "PKI Certificate Templates", + PkiCertificateCollections = "PKI Certificate Collections", + PkiAlerting = "PKI Alerting", + SshCertificates = "SSH Certificates", + SshCertificateAuthorities = "SSH Certificate Authorities", + SshCertificateTemplates = "SSH Certificate Templates", + KmsKeys = "KMS Keys", + KmsEncryption = "KMS Encryption", + KmsSigning = "KMS Signing" +} + export const GROUPS = { CREATE: { name: "The name of the group to create.", @@ -888,7 +933,7 @@ export const DYNAMIC_SECRETS = { environmentSlug: "The slug of the environment to list folders from.", path: "The path to list folders from." }, - LIST_LEAES_BY_NAME: { + LIST_LEASES_BY_NAME: { projectSlug: "The slug of the project to create dynamic secret in.", environmentSlug: "The slug of the environment to list folders from.", path: "The path to list folders from.", @@ -1812,6 +1857,20 @@ export const AppConnections = { WINDMILL: { instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).", accessToken: "The access token to use to connect with Windmill." + }, + LDAP: { + provider: "The type of LDAP provider. Determines provider-specific behaviors.", + url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').", + dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').", + password: "The password to bind with for authentication.", + sslRejectUnauthorized: + "Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.", + sslCertificate: + "The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate." + }, + TEAMCITY: { + instanceUrl: "The TeamCity instance URL to connect with.", + accessToken: "The access token to use to connect with TeamCity." } } }; @@ -1951,6 +2010,10 @@ export const SecretSyncs = { WINDMILL: { workspace: "The Windmill workspace to sync secrets to.", path: "The Windmill workspace path to sync secrets to." + }, + TEAMCITY: { + project: "The TeamCity project to sync secrets to.", + buildConfig: "The TeamCity build configuration to sync secrets to." } } }; @@ -2015,6 +2078,26 @@ export const SecretRotations = { }, AUTH0_CLIENT_SECRET: { clientId: "The client ID of the Auth0 Application to rotate the client secret for." + }, + LDAP_PASSWORD: { + dn: "The Distinguished Name (DN) of the principal to rotate the password for." + }, + GENERAL: { + PASSWORD_REQUIREMENTS: { + base: "The password requirements to use when generating the new password.", + length: "The length of the password to generate.", + required: { + digits: "The amount of digits to require in the generated password.", + lowercase: "The amount of lowercase characters to require in the generated password.", + uppercase: "The amount of uppercase characters to require in the generated password.", + symbols: "The amount of symbols to require in the generated password." + }, + allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").' + } + }, + AWS_IAM_USER_SECRET: { + userName: "The name of the client to rotate credentials for.", + region: "The AWS region the client is present in." } }, SECRETS_MAPPING: { @@ -2025,6 +2108,14 @@ export const SecretRotations = { AUTH0_CLIENT_SECRET: { clientId: "The name of the secret that the client ID will be mapped to.", clientSecret: "The name of the secret that the rotated client secret will be mapped to." + }, + LDAP_PASSWORD: { + dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.", + password: "The name of the secret that the rotated password will be mapped to." + }, + AWS_IAM_USER_SECRET: { + accessKeyId: "The name of the secret that the access key ID will be mapped to.", + secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to." } } }; diff --git a/backend/src/lib/base64/index.ts b/backend/src/lib/base64/index.ts index 2bffef3fc..dfdd03d47 100644 --- a/backend/src/lib/base64/index.ts +++ b/backend/src/lib/base64/index.ts @@ -1,12 +1,16 @@ +import RE2 from "re2"; + type Base64Options = { urlSafe?: boolean; padding?: boolean; }; -const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/; -const base64WithoutPadding = /^[A-Za-z0-9+/]+$/; -const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/; -const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/; +const base64WithPadding = new RE2(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/); +const base64WithoutPadding = new RE2(/^[A-Za-z0-9+/]+$/); +const base64UrlWithPadding = new RE2( + /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/ +); +const base64UrlWithoutPadding = new RE2(/^[A-Za-z0-9_-]+$/); export const isBase64 = (str: string, options: Base64Options = {}): boolean => { if (typeof str !== "string") { diff --git a/backend/src/lib/fn/string.ts b/backend/src/lib/fn/string.ts index 1dc2bbfed..2fa4c9166 100644 --- a/backend/src/lib/fn/string.ts +++ b/backend/src/lib/fn/string.ts @@ -1,4 +1,5 @@ import path from "path"; +import RE2 from "re2"; // given two paths irrespective of ending with / or not // this will return true if its equal @@ -15,4 +16,6 @@ export const prefixWithSlash = (str: string) => { return `/${str}`; }; -export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str); +const vowelRegex = new RE2(/^[aeiou]/i); + +export const startsWithVowel = (str: string) => vowelRegex.test(str); diff --git a/backend/src/lib/regex/index.ts b/backend/src/lib/regex/index.ts new file mode 100644 index 000000000..68ba7671d --- /dev/null +++ b/backend/src/lib/regex/index.ts @@ -0,0 +1,3 @@ +export const DistinguishedNameRegex = + // DN format, ie; CN=user,OU=users,DC=example,DC=com + /^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/; diff --git a/backend/src/lib/validator/validate-string.ts b/backend/src/lib/validator/validate-string.ts index d2d033693..4e89d313f 100644 --- a/backend/src/lib/validator/validate-string.ts +++ b/backend/src/lib/validator/validate-string.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z } from "zod"; export enum CharacterType { @@ -92,7 +93,7 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => { const combinedPattern = allowedCharacters.map((char) => patternMap[char]).join(""); // Create a regex that matches only the allowed characters - const regex = new RegExp(`^[${combinedPattern}]+$`); + const regex = new RE2(`^[${combinedPattern}]+$`); /** * Validates if the input string contains only the allowed character types diff --git a/backend/src/lib/validator/validate-url.ts b/backend/src/lib/validator/validate-url.ts index fdf99e405..b555869d7 100644 --- a/backend/src/lib/validator/validate-url.ts +++ b/backend/src/lib/validator/validate-url.ts @@ -1,6 +1,7 @@ import dns from "node:dns/promises"; import { isIPv4 } from "net"; +import RE2 from "re2"; import { getConfig } from "@app/lib/config/env"; @@ -80,42 +81,47 @@ export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => { if ( !opts.allow_numeric_tld && - !/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(tld) + !new RE2(/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i).test(tld) ) { return false; } // disallow spaces - if (/\s/.test(tld)) { + if (new RE2(/\s/).test(tld)) { return false; } } // reject numeric TLDs - if (!opts.allow_numeric_tld && /^\d+$/.test(tld)) { + if (!opts.allow_numeric_tld && new RE2(/^\d+$/).test(tld)) { return false; } + const partRegex = new RE2(/^[a-z_\u00a1-\uffff0-9-]+$/i); + const fullWidthRegex = new RE2(/[\uff01-\uff5e]/); + const hyphenRegex = new RE2(/^-|-$/); + const underscoreRegex = new RE2(/_/); + return parts.every((part) => { if (part.length > 63 && !opts.ignore_max_length) { return false; } - if (!/^[a-z_\u00a1-\uffff0-9-]+$/i.test(part)) { + if (!partRegex.test(part)) { return false; } // disallow full-width chars - if (/[\uff01-\uff5e]/.test(part)) { + if (fullWidthRegex.test(part)) { return false; } // disallow parts starting or ending with hyphen - if (/^-|-$/.test(part)) { + if (hyphenRegex.test(part)) { return false; } - if (!opts.allow_underscores && /_/.test(part)) { + if (!opts.allow_underscores && underscoreRegex.test(part)) { return false; } diff --git a/backend/src/lib/zod/index.ts b/backend/src/lib/zod/index.ts index 4d3fea8c7..21c59b6ba 100644 --- a/backend/src/lib/zod/index.ts +++ b/backend/src/lib/zod/index.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z, ZodTypeAny } from "zod"; // this is a patched zod string to remove empty string to undefined @@ -11,3 +12,8 @@ export const zpStr = (schema: T, opt: { stripNull: boolean export const zodBuffer = z.custom((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, { message: "Expected binary data (Buffer Or Uint8Array)" }); + +export const re2Validator = (pattern: string | RegExp) => { + const re2Pattern = new RE2(pattern); + return (value: string) => re2Pattern.test(value); +}; diff --git a/backend/src/main.ts b/backend/src/main.ts index 80d98abcf..c3b5a0900 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -73,6 +73,7 @@ const run = async () => { // eslint-disable-next-line process.on("SIGINT", async () => { await server.close(); + await queue.shutdown(); await db.destroy(); await removeTemporaryBaseDirectory(); hsmModule.finalize(); @@ -82,19 +83,22 @@ const run = async () => { // eslint-disable-next-line process.on("SIGTERM", async () => { await server.close(); + await queue.shutdown(); await db.destroy(); await removeTemporaryBaseDirectory(); hsmModule.finalize(); process.exit(0); }); - process.on("uncaughtException", (error) => { - logger.error(error, "CRITICAL ERROR: Uncaught Exception"); - }); + if (!envConfig.isDevelopmentMode) { + process.on("uncaughtException", (error) => { + logger.error(error, "CRITICAL ERROR: Uncaught Exception"); + }); - process.on("unhandledRejection", (error) => { - logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection"); - }); + process.on("unhandledRejection", (error) => { + logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection"); + }); + } await server.listen({ port: envConfig.PORT, diff --git a/backend/src/server/plugins/fastify-zod.ts b/backend/src/server/plugins/fastify-zod.ts index 02636a066..4e898a9b5 100644 --- a/backend/src/server/plugins/fastify-zod.ts +++ b/backend/src/server/plugins/fastify-zod.ts @@ -49,14 +49,17 @@ function resolveSchema(maybeSchema: ZodAny | { properties: ZodAny }): Pick { - return ({ schema, url }: { schema: Schema; url: string }) => { + return ({ schema = {}, url }: { schema: Schema; url: string }) => { if (!schema) { return { - schema, + schema: { hide: true }, url }; } + if (typeof schema.hide === "undefined") { + schema.hide = true; + } const { response, headers, querystring, body, params, hide, ...rest } = schema; const transformed: FreeformRecord = {}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 0a6b652cd..3d95a81eb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -603,7 +603,14 @@ export const registerRoutes = async ( kmsService }); - const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService }); + const loginService = authLoginServiceFactory({ + userDAL, + smtpService, + tokenService, + orgDAL, + totpService, + auditLogService + }); const passwordService = authPaswordServiceFactory({ tokenService, smtpService, @@ -1098,7 +1105,8 @@ export const registerRoutes = async ( secretApprovalRequestSecretDAL, kmsService, snapshotService, - resourceMetadataDAL + resourceMetadataDAL, + keyStore }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts index dfb451a4c..0bc8f7c59 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { AppConnections } from "@app/lib/api-docs"; +import { ApiDocsTags, AppConnections } from "@app/lib/api-docs"; import { startsWithVowel } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -43,6 +43,8 @@ export const registerAppConnectionEndpoints = { @@ -86,6 +96,8 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], description: "List the available App Connection Options.", response: { 200: z.object({ @@ -107,6 +119,8 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], description: "List all the App Connections for the current organization.", response: { 200: z.object({ appConnections: SanitizedAppConnectionSchema.array() }) diff --git a/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts index 674e6e417..3226a6aa8 100644 --- a/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts @@ -59,4 +59,40 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => return { kmsKeys }; } }); + + server.route({ + method: "GET", + url: `/:connectionId/users`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + iamUsers: z + .object({ + UserName: z.string(), + Arn: z.string() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const iamUsers = await server.services.appConnection.aws.listIamUsers( + { + connectionId + }, + req.permission + ); + + return { iamUsers }; + } + }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index a833b6882..c2398fd78 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,6 +1,6 @@ -import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { registerAuth0ConnectionRouter } from "./auth0-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; @@ -9,8 +9,10 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; +import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; +import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerWindmillConnectionRouter } from "./windmill-connection-router"; @@ -32,5 +34,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.LDAP, + server, + sanitizedResponseSchema: SanitizedLdapConnectionSchema, + createSchema: CreateLdapConnectionSchema, + updateSchema: UpdateLdapConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/teamcity-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/teamcity-connection-router.ts new file mode 100644 index 000000000..c794c36ca --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/teamcity-connection-router.ts @@ -0,0 +1,60 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateTeamCityConnectionSchema, + SanitizedTeamCityConnectionSchema, + UpdateTeamCityConnectionSchema +} from "@app/services/app-connection/teamcity"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerTeamCityConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.TeamCity, + server, + sanitizedResponseSchema: SanitizedTeamCityConnectionSchema, + createSchema: CreateTeamCityConnectionSchema, + updateSchema: UpdateTeamCityConnectionSchema + }); + + // The following endpoints are for internal Infisical App use only and not part of the public API + server.route({ + method: "GET", + url: `/:connectionId/projects`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + buildTypes: z.object({ + buildType: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + }) + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const projects = await server.services.appConnection.teamcity.listProjects(connectionId, req.permission); + + return projects; + } + }); +}; diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index b108215ca..f6538b797 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -26,6 +26,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Create CA", body: z .object({ @@ -105,6 +107,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET.caId) @@ -151,6 +155,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get DER-encoded certificate of CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caId), @@ -177,6 +183,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Update CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId) @@ -231,6 +239,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Delete CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.DELETE.caId) @@ -276,6 +286,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get CA CSR", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CSR.caId) @@ -321,6 +333,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Perform CA certificate renewal", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) @@ -376,6 +390,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of past and current CA certificates for a CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId) @@ -424,6 +440,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get current CA cert and cert chain of a CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId) @@ -473,6 +491,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Create intermediate CA certificate from parent CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId) @@ -536,6 +556,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Import certificate and chain to CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.caId) @@ -588,6 +610,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Issue certificate from CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId) @@ -679,6 +703,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Sign certificate from CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) @@ -770,6 +796,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of certificate templates for the CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) @@ -815,6 +843,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of CRLs of the CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId) diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 3101393bd..ea33e948f 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { CertificatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; +import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -24,6 +24,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Get certificate", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) @@ -70,6 +72,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Issue certificate", body: z .object({ @@ -181,6 +185,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Sign certificate", body: z .object({ @@ -292,6 +298,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Revoke", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber) @@ -346,6 +354,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Delete certificate", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber) @@ -392,6 +402,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Get certificate body of certificate", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index 2217445cb..b0c186206 100644 --- a/backend/src/server/routes/v1/certificate-template-router.ts +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; +import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -26,6 +26,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], params: z.object({ certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId) }), @@ -65,6 +67,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], body: z.object({ caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId), @@ -132,6 +136,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], body: z.object({ caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId), @@ -198,6 +204,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], params: z.object({ certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId) }), @@ -238,6 +246,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], description: "Create Certificate Template EST configuration", params: z.object({ certificateTemplateId: z.string().trim() @@ -292,6 +302,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], description: "Update Certificate Template EST configuration", params: z.object({ certificateTemplateId: z.string().trim() @@ -340,6 +352,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], description: "Get Certificate Template EST configuration", params: z.object({ certificateTemplateId: z.string().trim() diff --git a/backend/src/server/routes/v1/cmek-router.ts b/backend/src/server/routes/v1/cmek-router.ts index 64f47f980..c8fd485a7 100644 --- a/backend/src/server/routes/v1/cmek-router.ts +++ b/backend/src/server/routes/v1/cmek-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { KMS } from "@app/lib/api-docs"; +import { ApiDocsTags, KMS } from "@app/lib/api-docs"; import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64"; import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign"; @@ -46,6 +46,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Create KMS key", body: z .object({ @@ -138,6 +140,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Update KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.UPDATE_KEY.keyId) @@ -187,6 +191,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Delete KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.DELETE_KEY.keyId) @@ -229,6 +235,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "List KMS keys", querystring: z.object({ projectId: z.string().describe(KMS.LIST_KEYS.projectId), @@ -280,6 +288,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Get KMS key by ID", params: z.object({ keyId: z.string().uuid().describe(KMS.GET_KEY_BY_ID.keyId) @@ -321,6 +331,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Get KMS key by name", params: z.object({ keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName) @@ -367,6 +379,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsEncryption], description: "Encrypt data with KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId) @@ -412,6 +426,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], description: "Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.", params: z.object({ @@ -454,6 +470,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], description: "List all available signing algorithms for a KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId) @@ -495,6 +513,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], description: "Sign data with a KMS key.", params: z.object({ keyId: z.string().uuid().describe(KMS.SIGN.keyId) @@ -548,6 +568,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], description: "Verify data signatures with a KMS key.", params: z.object({ keyId: z.string().uuid().describe(KMS.VERIFY.keyId) @@ -604,6 +626,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsEncryption], description: "Decrypt data with KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.DECRYPT.keyId) diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 7ed62e679..20ed8d150 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { writeLimit } from "@app/server/config/rateLimiter"; export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvider) => { @@ -11,6 +11,8 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Renew access token", body: z.object({ accessToken: z.string().trim().describe(UNIVERSAL_AUTH.RENEW_ACCESS_TOKEN.accessToken) @@ -44,6 +46,8 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Revoke access token", body: z.object({ accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken) diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 82387a3cc..effd66a68 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityAwsAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { AWS_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, AWS_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -21,6 +21,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Login with AWS Auth", body: z.object({ identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId), @@ -71,6 +73,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Attach AWS Auth configuration onto identity", security: [ { @@ -165,6 +169,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Update AWS Auth configuration on identity", security: [ { @@ -247,6 +253,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Retrieve AWS Auth configuration on identity", security: [ { @@ -293,6 +301,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Delete AWS Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index 1cf59e682..9053bc2e3 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityAzureAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { AZURE_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, AZURE_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,6 +18,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Login with Azure Auth", body: z.object({ identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId), @@ -66,6 +68,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Attach Azure Auth configuration onto identity", security: [ { @@ -159,6 +163,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Update Azure Auth configuration on identity", security: [ { @@ -247,6 +253,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Retrieve Azure Auth configuration on identity", security: [ { @@ -294,6 +302,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Delete Azure Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index d269072d9..b83faf9d9 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityGcpAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { GCP_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, GCP_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,9 +18,11 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Login with GCP Auth", body: z.object({ - identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId).trim(), + identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId), jwt: z.string() }), response: { @@ -66,6 +68,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Attach GCP Auth configuration onto identity", security: [ { @@ -157,6 +161,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Update GCP Auth configuration on identity", security: [ { @@ -241,6 +247,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Retrieve GCP Auth configuration on identity", security: [ { @@ -288,6 +296,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Delete GCP Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index bb09898a3..373a8b927 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityJwtAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { JWT_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, JWT_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -94,6 +94,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], description: "Login with JWT Auth", body: z.object({ identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId), @@ -144,6 +146,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], description: "Attach JWT Auth configuration onto identity", security: [ { @@ -211,6 +215,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], description: "Update JWT Auth configuration on identity", security: [ { @@ -275,6 +281,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], description: "Retrieve JWT Auth configuration on identity", security: [ { @@ -322,6 +330,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], description: "Delete JWT Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 0105afd76..21759e0cd 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityKubernetesAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { KUBERNETES_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -35,6 +35,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Login with Kubernetes Auth", body: z.object({ identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId), @@ -85,6 +87,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Attach Kubernetes Auth configuration onto identity", security: [ { @@ -182,6 +186,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Update Kubernetes Auth configuration on identity", security: [ { @@ -278,6 +284,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Retrieve Kubernetes Auth configuration on identity", security: [ { @@ -325,6 +333,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Delete Kubernetes Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 4a7b66146..74cd94eb5 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityOidcAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { OIDC_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, OIDC_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -42,6 +42,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Login with OIDC Auth", body: z.object({ identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId), @@ -96,6 +98,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Attach OIDC Auth configuration onto identity", security: [ { @@ -195,6 +199,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Update OIDC Auth configuration on identity", security: [ { @@ -292,6 +298,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Retrieve OIDC Auth configuration on identity", security: [ { @@ -339,6 +347,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Delete OIDC Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 107a4b9ef..7731aad98 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { IDENTITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, IDENTITIES } from "@app/lib/api-docs"; import { buildSearchZodSchema, SearchResourceOperators } from "@app/lib/search-resource/search"; import { OrderByDirection } from "@app/lib/types"; import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string"; @@ -32,6 +32,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Create identity", security: [ { @@ -100,6 +102,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Update identity", security: [ { @@ -158,6 +162,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Delete identity", security: [ { @@ -205,6 +211,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Get an identity by id", security: [ { @@ -260,6 +268,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "List identities", security: [ { @@ -308,6 +318,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Search identities", security: [ { diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index d6e7259be..e22c41889 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { TOKEN_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, TOKEN_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,6 +18,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Attach Token Auth configuration onto identity", security: [ { @@ -108,6 +110,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Update Token Auth configuration on identity", security: [ { @@ -192,6 +196,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Retrieve Token Auth configuration on identity", security: [ { @@ -239,6 +245,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Delete Token Auth configuration on identity", security: [ { @@ -287,6 +295,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Create token for identity with Token Auth", security: [ { @@ -349,6 +359,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Get tokens for identity with Token Auth", security: [ { @@ -402,6 +414,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Update token for identity with Token Auth", security: [ { @@ -456,6 +470,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Revoke token for identity with Token Auth", security: [ { diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index 5a9363f3d..6fe4c7a85 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -30,6 +30,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Login with Universal Auth", body: z.object({ clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId), @@ -78,6 +80,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Attach Universal Auth configuration onto identity", security: [ { @@ -175,6 +179,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Update Universal Auth configuration on identity", security: [ { @@ -271,6 +277,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Retrieve Universal Auth configuration on identity", security: [ { @@ -318,6 +326,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Delete Universal Auth configuration on identity", security: [ { @@ -365,6 +375,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Create Universal Auth Client Secret for identity", security: [ { @@ -421,6 +433,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "List Universal Auth Client Secrets for identity", security: [ { @@ -469,6 +483,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Get Universal Auth Client Secret for identity", security: [ { @@ -519,6 +535,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Revoke Universal Auth Client Secrets for identity", security: [ { diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index bca11cbf3..e5156724d 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { INTEGRATION_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, INTEGRATION_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -19,6 +19,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "List of integrations available.", security: [ { @@ -57,6 +59,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Get details of an integration authorization by auth object id.", security: [ { @@ -92,6 +96,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Update the integration authentication object required for syncing secrets.", security: [ { @@ -153,6 +159,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Remove all integration's auth object from the project.", security: [ { @@ -202,6 +210,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Remove an integration auth object by object id.", security: [ { @@ -294,6 +304,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Create the integration authentication object required for syncing secrets.", security: [ { diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index f0c28478c..f3964e7b7 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IntegrationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { INTEGRATION } from "@app/lib/api-docs"; +import { ApiDocsTags, INTEGRATION } from "@app/lib/api-docs"; import { removeTrailingSlash, shake } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -22,6 +22,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Create an integration to sync secrets.", security: [ { @@ -119,6 +121,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Update an integration by integration id", security: [ { @@ -178,6 +182,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Get an integration by integration id", security: [ { @@ -247,6 +253,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Remove an integration using the integration object ID", security: [ { @@ -315,6 +323,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Manually trigger sync of an integration by integration id", security: [ { diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 90ca3d255..22314b54b 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -9,7 +9,7 @@ import { UsersSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; +import { ApiDocsTags, AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas"; @@ -109,6 +109,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AuditLogs], description: "Get all audit logs for an organization", querystring: z.object({ projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), diff --git a/backend/src/server/routes/v1/pki-alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts index f64ec9e47..43ce91e88 100644 --- a/backend/src/server/routes/v1/pki-alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { PkiAlertsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ALERTS } from "@app/lib/api-docs"; +import { ALERTS, ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -16,6 +16,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Create PKI alert", body: z.object({ projectId: z.string().trim().describe(ALERTS.CREATE.projectId), @@ -68,6 +70,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Get PKI alert", params: z.object({ alertId: z.string().trim().describe(ALERTS.GET.alertId) @@ -108,6 +112,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Update PKI alert", params: z.object({ alertId: z.string().trim().describe(ALERTS.UPDATE.alertId) @@ -164,6 +170,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Delete PKI alert", params: z.object({ alertId: z.string().trim().describe(ALERTS.DELETE.alertId) diff --git a/backend/src/server/routes/v1/pki-collection-router.ts b/backend/src/server/routes/v1/pki-collection-router.ts index 2f2add5c1..92f883b38 100644 --- a/backend/src/server/routes/v1/pki-collection-router.ts +++ b/backend/src/server/routes/v1/pki-collection-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PKI_COLLECTIONS } from "@app/lib/api-docs"; +import { ApiDocsTags, PKI_COLLECTIONS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -17,6 +17,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Create PKI collection", body: z.object({ projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId), @@ -60,6 +62,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Get PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.GET.collectionId) @@ -100,6 +104,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Update PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId) @@ -146,6 +152,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Delete PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE.collectionId) @@ -186,6 +194,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Get items in PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.LIST_ITEMS.collectionId) @@ -247,6 +257,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Add item to PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.collectionId) @@ -298,6 +310,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Remove item from PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionId), diff --git a/backend/src/server/routes/v1/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index 705016696..9a136e160 100644 --- a/backend/src/server/routes/v1/project-env-router.ts +++ b/backend/src/server/routes/v1/project-env-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { ProjectEnvironmentsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ENVIRONMENTS } from "@app/lib/api-docs"; +import { ApiDocsTags, ENVIRONMENTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -13,9 +13,11 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { method: "GET", url: "/:workspaceId/environments/:envId", config: { - rateLimit: writeLimit + rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Get Environment", security: [ { @@ -65,6 +67,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Get Environment by ID", security: [ { @@ -112,6 +116,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Create environment", security: [ { @@ -171,6 +177,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Update environment", security: [ { @@ -237,6 +245,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Delete environment", security: [ { diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index b5a4a8677..cd3734efc 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -8,7 +8,7 @@ import { UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PROJECT_USERS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -23,6 +23,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Return project user memberships", security: [ { @@ -141,6 +143,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Return project user memberships", security: [ { @@ -255,6 +259,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Update project user membership", security: [ { diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 4182ce389..7df68f39a 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -1,3 +1,4 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { @@ -6,6 +7,7 @@ import { ProjectMembershipsSchema, ProjectRolesSchema, ProjectSlackConfigsSchema, + ProjectSshConfigsSchema, ProjectType, SecretFoldersSchema, SortDirection, @@ -13,8 +15,9 @@ import { UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { re2Validator } from "@app/lib/zod"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -77,7 +80,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { includeGroupMembers: z .enum(["true", "false"]) .default("false") - .transform((value) => value === "true") + .transform((value) => value === "true"), + roles: z + .string() + .trim() + .transform(decodeURIComponent) + .refine((value) => { + if (!value) return true; + const slugs = value.split(","); + return slugs.every((slug) => slugify(slug.trim(), { lowercase: true }) === slug.trim()); + }) + .optional() }), params: z.object({ workspaceId: z.string().trim() @@ -116,13 +129,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + const roles = (req.query.roles?.split(",") || []).filter(Boolean); const users = await server.services.projectMembership.getProjectMemberships({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, includeGroupMembers: req.query.includeGroupMembers, projectId: req.params.workspaceId, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + roles }); return { users }; @@ -176,6 +191,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Get project", security: [ { @@ -214,6 +231,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Delete project", security: [ { @@ -289,6 +308,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Update project", security: [ { @@ -316,11 +337,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { slug: z .string() .trim() - .regex( - /^[a-z0-9]+(?:[_-][a-z0-9]+)*$/, - "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore." - ) .max(64, { message: "Slug must be 64 characters or fewer" }) + .refine(re2Validator(/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/), { + message: + "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore." + }) .optional() .describe(PROJECTS.UPDATE.slug) }), @@ -512,6 +533,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "List integrations for a project.", security: [ { @@ -555,6 +578,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "List integration auth objects for a workspace.", security: [ { @@ -612,6 +637,107 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:workspaceId/ssh-config", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: ProjectSshConfigsSchema.pick({ + id: true, + createdAt: true, + updatedAt: true, + projectId: true, + defaultUserSshCaId: true, + defaultHostSshCaId: true + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const sshConfig = await server.services.project.getProjectSshConfig({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: sshConfig.projectId, + event: { + type: EventType.GET_PROJECT_SSH_CONFIG, + metadata: { + id: sshConfig.id, + projectId: sshConfig.projectId + } + } + }); + + return sshConfig; + } + }); + + server.route({ + method: "PATCH", + url: "/:workspaceId/ssh-config", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + defaultUserSshCaId: z.string().optional(), + defaultHostSshCaId: z.string().optional() + }), + response: { + 200: ProjectSshConfigsSchema.pick({ + id: true, + createdAt: true, + updatedAt: true, + projectId: true, + defaultUserSshCaId: true, + defaultHostSshCaId: true + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const sshConfig = await server.services.project.updateProjectSshConfig({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: sshConfig.projectId, + event: { + type: EventType.UPDATE_PROJECT_SSH_CONFIG, + metadata: { + id: sshConfig.id, + projectId: sshConfig.projectId, + defaultUserSshCaId: sshConfig.defaultUserSshCaId, + defaultHostSshCaId: sshConfig.defaultHostSshCaId + } + } + }); + + return sshConfig; + } + }); + server.route({ method: "GET", url: "/:workspaceId/slack-config", diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index dbfa715ea..b307347b8 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { SecretFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { FOLDERS } from "@app/lib/api-docs"; +import { ApiDocsTags, FOLDERS } from "@app/lib/api-docs"; import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn"; import { isValidFolderName } from "@app/lib/validator"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; @@ -19,6 +19,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Create folders", security: [ { @@ -98,6 +100,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Update folder", security: [ { @@ -181,6 +185,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Update folders by batch", security: [ { @@ -259,6 +265,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Delete a folder", security: [ { @@ -332,6 +340,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Get folders", security: [ { @@ -390,6 +400,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Get folder by id", security: [ { diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v1/secret-import-router.ts index aa6efdf36..fca11f8a0 100644 --- a/backend/src/server/routes/v1/secret-import-router.ts +++ b/backend/src/server/routes/v1/secret-import-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { SECRET_IMPORTS } from "@app/lib/api-docs"; +import { ApiDocsTags, SECRET_IMPORTS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -18,6 +18,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Create secret imports", security: [ { @@ -83,6 +85,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Update secret imports", security: [ { @@ -157,6 +161,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Delete secret imports", security: [ { @@ -263,6 +269,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Get secret imports", security: [ { @@ -319,6 +327,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Get single secret import", security: [ { @@ -421,6 +431,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], querystring: z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index ee407cee3..a54777727 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; +import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerWindmillSyncRouter } from "./windmill-sync-router"; @@ -27,5 +28,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { @@ -65,6 +68,8 @@ export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], description: "List the available Secret Sync Options.", response: { 200: z.object({ @@ -86,6 +91,8 @@ export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], description: "List all the Secret Syncs for the specified project.", querystring: z.object({ projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST().projectId) diff --git a/backend/src/server/routes/v1/secret-sync-routers/teamcity-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/teamcity-sync-router.ts new file mode 100644 index 000000000..a3091aae5 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/teamcity-sync-router.ts @@ -0,0 +1,17 @@ +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + CreateTeamCitySyncSchema, + TeamCitySyncSchema, + UpdateTeamCitySyncSchema +} from "@app/services/secret-sync/teamcity"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerTeamCitySyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.TeamCity, + server, + responseSchema: TeamCitySyncSchema, + createSchema: CreateTeamCitySyncSchema, + updateSchema: UpdateTeamCitySyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index ed9837084..01ba783fe 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { SecretTagsSchema } from "@app/db/schemas"; -import { SECRET_TAGS } from "@app/lib/api-docs"; +import { ApiDocsTags, SECRET_TAGS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -15,6 +15,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId) }), @@ -44,6 +46,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId), tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId) @@ -75,6 +79,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId), tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug) @@ -107,6 +113,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) }), @@ -141,6 +149,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId), tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) @@ -176,6 +186,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId), tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId) diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 47456e622..5a081a3d9 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -6,7 +6,7 @@ import { ProjectMembershipRole, ProjectUserMembershipRolesSchema } from "@app/db/schemas"; -import { PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -22,6 +22,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Add group to project", security: [ { @@ -88,6 +90,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => url: "/:projectId/groups/:groupId", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Update group in project", security: [ { @@ -147,6 +151,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Remove group from project", security: [ { @@ -185,6 +191,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Return list of groups in project", security: [ { @@ -243,6 +251,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Return project group", security: [ { diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 52940eb44..8680a2dca 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; -import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { ApiDocsTags, ORGANIZATIONS } from "@app/lib/api-docs"; import { OrderByDirection } from "@app/lib/types"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -17,6 +17,8 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Return organization identity memberships", security: [ { diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index 4205d9326..9de7ed1aa 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -6,7 +6,7 @@ import { ProjectMembershipRole, ProjectUserMembershipRolesSchema } from "@app/db/schemas"; -import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { OrderByDirection } from "@app/lib/types"; @@ -27,6 +27,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Create project identity membership", security: [ { @@ -101,6 +103,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Update project identity memberships", security: [ { @@ -170,6 +174,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Delete project identity memberships", security: [ { @@ -207,6 +213,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Return project identity memberships", security: [ { @@ -300,6 +308,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Return project identity membership", security: [ { @@ -360,6 +370,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], params: z.object({ identityMembershipId: z.string().trim() }), diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 90b500a5a..504359726 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -8,7 +8,7 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; -import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { ApiDocsTags, ORGANIZATIONS } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { GenericResourceNameSchema } from "@app/server/lib/schemas"; @@ -24,6 +24,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Return organization user memberships", security: [ { @@ -72,6 +74,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Return projects in organization that user is apart of", security: [ { @@ -179,6 +183,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Update organization user memberships", security: [ { @@ -229,6 +235,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Delete organization user memberships", security: [ { diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 0c3ceb01e..a1a1cfc96 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PROJECT_USERS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -15,6 +15,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Invite members to project", security: [ { @@ -78,6 +80,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Remove members from project", security: [ { diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index c8f68a926..f7540591e 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -14,7 +14,7 @@ import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-s import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema"; import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema"; -import { PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -29,7 +29,8 @@ import { SanitizedProjectSchema } from "../sanitizedSchemas"; const projectWithEnv = SanitizedProjectSchema.extend({ _id: z.string(), - environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() + environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array(), + kmsSecretManagerKeyId: z.string().nullable().optional() }); export const registerProjectRouter = async (server: FastifyZodProvider) => { @@ -150,6 +151,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Create a new project", security: [ { @@ -224,6 +227,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Delete project", security: [ { @@ -342,6 +347,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], params: z.object({ slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug) }), @@ -383,6 +390,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], params: z.object({ slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug) }), @@ -551,6 +560,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], params: z.object({ projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CERTIFICATE_TEMPLATES.projectId) }), @@ -581,6 +592,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], params: z.object({ projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId) }), diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index fb10f17db..aa6165d90 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { ServiceTokensSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -25,6 +26,8 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.SERVICE_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ServiceTokens], description: "Return Infisical Token data", security: [ { diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 851d9c4ff..027f527fc 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -252,6 +252,31 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "DELETE", + url: "/me/sessions/:sessionId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + sessionId: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.authToken.revokeMySessionById(req.permission.id, req.params.sessionId); + return { + message: "Successfully revoked session" + }; + } + }); + server.route({ method: "GET", url: "/me", diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 0f53b777a..40aed624b 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; +import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -48,6 +48,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Attach tags to a secret", security: [ { @@ -103,6 +105,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Detach tags from a secret", security: [ { @@ -158,6 +162,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "List secrets", security: [ { @@ -355,6 +361,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], params: z.object({ secretId: z.string() }), @@ -390,6 +398,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Get a secret by name", security: [ { @@ -496,6 +506,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Create secret", security: [ { @@ -609,6 +621,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Update secret", security: [ { @@ -729,6 +743,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Delete secret", security: [ { @@ -1486,6 +1502,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], body: z.object({ projectSlug: z.string().trim(), sourceEnvironment: z.string().trim(), @@ -1911,6 +1929,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Create many secrets", security: [ { @@ -2017,6 +2037,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Update many secrets", security: [ { @@ -2170,6 +2192,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Delete many secrets", security: [ { @@ -2266,6 +2290,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Get secret reference tree", security: [ { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 6b6048f2a..de20f3f64 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -12,7 +12,9 @@ export enum AppConnection { MsSql = "mssql", Camunda = "camunda", Windmill = "windmill", - Auth0 = "auth0" + Auth0 = "auth0", + LDAP = "ldap", + TeamCity = "teamcity" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 7e08a92b4..92595619c 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -41,8 +41,14 @@ import { HumanitecConnectionMethod, validateHumanitecConnectionCredentials } from "./humanitec"; +import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; +import { + getTeamCityConnectionListItem, + TeamCityConnectionMethod, + validateTeamCityConnectionCredentials +} from "./teamcity"; import { getTerraformCloudConnectionListItem, TerraformCloudConnectionMethod, @@ -71,7 +77,9 @@ export const listAppConnectionOptions = () => { getMsSqlConnectionListItem(), getCamundaConnectionListItem(), getWindmillConnectionListItem(), - getAuth0ConnectionListItem() + getAuth0ConnectionListItem(), + getLdapConnectionListItem(), + getTeamCityConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -135,7 +143,9 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -167,9 +177,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case MsSqlConnectionMethod.UsernameAndPassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: + case TeamCityConnectionMethod.AccessToken: return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; + case LdapConnectionMethod.SimpleBind: + return "Simple Bind"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); @@ -214,5 +227,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Vercel]: platformManagedCredentialsNotSupported, [AppConnection.Windmill]: platformManagedCredentialsNotSupported, - [AppConnection.Auth0]: platformManagedCredentialsNotSupported + [AppConnection.Auth0]: platformManagedCredentialsNotSupported, + [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future + [AppConnection.TeamCity]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 762a9bcf2..524993b23 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -14,5 +14,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.Camunda]: "Camunda", [AppConnection.Windmill]: "Windmill", - [AppConnection.Auth0]: "Auth0" + [AppConnection.Auth0]: "Auth0", + [AppConnection.LDAP]: "LDAP", + [AppConnection.TeamCity]: "TeamCity" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5293761a8..e872c07e4 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -43,8 +43,11 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { githubConnectionService } from "./github/github-connection-service"; import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; +import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; +import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; +import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service"; import { ValidateVercelConnectionCredentialsSchema } from "./vercel"; @@ -74,7 +77,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record>>; @@ -118,6 +132,8 @@ export type TAppConnectionInput = { id: string } & ( | TCamundaConnectionInput | TWindmillConnectionInput | TAuth0ConnectionInput + | TLdapConnectionInput + | TTeamCityConnectionInput ); export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; @@ -144,7 +160,9 @@ export type TAppConnectionConfig = | TSqlConnectionConfig | TCamundaConnectionConfig | TWindmillConnectionConfig - | TAuth0ConnectionConfig; + | TAuth0ConnectionConfig + | TLdapConnectionConfig + | TTeamCityConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -160,7 +178,9 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateTerraformCloudConnectionCredentialsSchema | TValidateVercelConnectionCredentialsSchema | TValidateWindmillConnectionCredentialsSchema - | TValidateAuth0ConnectionCredentialsSchema; + | TValidateAuth0ConnectionCredentialsSchema + | TValidateLdapConnectionCredentialsSchema + | TValidateTeamCityConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; @@ -168,6 +188,10 @@ export type TListAwsConnectionKmsKeys = { destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager; }; +export type TListAwsConnectionIamUsers = { + connectionId: string; +}; + export type TAppConnectionCredentialsValidator = ( appConnection: TAppConnectionConfig ) => Promise; diff --git a/backend/src/services/app-connection/aws/aws-connection-fns.ts b/backend/src/services/app-connection/aws/aws-connection-fns.ts index 767cb82fb..28660173b 100644 --- a/backend/src/services/app-connection/aws/aws-connection-fns.ts +++ b/backend/src/services/app-connection/aws/aws-connection-fns.ts @@ -1,9 +1,11 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; import AWS from "aws-sdk"; +import { AxiosError } from "axios"; import { randomUUID } from "crypto"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; import { AwsConnectionMethod } from "./aws-connection-enums"; @@ -90,9 +92,20 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec const sts = new AWS.STS(awsConfig); resp = await sts.getCallerIdentity().promise(); - } catch (e: unknown) { + } catch (error: unknown) { + logger.error(error, "Error validating AWS connection credentials"); + + let message: string; + + if (error instanceof AxiosError) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + message = (error.response?.data?.message as string) || error.message || "verify credentials"; + } else { + message = (error as Error)?.message || "verify credentials"; + } + throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: `Unable to validate connection: ${message}` }); } diff --git a/backend/src/services/app-connection/aws/aws-connection-service.ts b/backend/src/services/app-connection/aws/aws-connection-service.ts index 689608b81..369116a9c 100644 --- a/backend/src/services/app-connection/aws/aws-connection-service.ts +++ b/backend/src/services/app-connection/aws/aws-connection-service.ts @@ -2,7 +2,10 @@ import AWS from "aws-sdk"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; -import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types"; +import { + TListAwsConnectionIamUsers, + TListAwsConnectionKmsKeys +} from "@app/services/app-connection/app-connection-types"; import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; @@ -70,6 +73,23 @@ const listAwsKmsKeys = async ( return kmsKeys; }; +const listAwsIamUsers = async (appConnection: TAwsConnection) => { + const { credentials } = await getAwsConnectionConfig(appConnection); + + const iam = new AWS.IAM({ credentials }); + + const userEntries: AWS.IAM.User[] = []; + let userMarker: string | undefined; + do { + // eslint-disable-next-line no-await-in-loop + const response = await iam.listUsers({ MaxItems: 100, Marker: userMarker }).promise(); + userEntries.push(...(response.Users || [])); + userMarker = response.Marker; + } while (userMarker); + + return userEntries; +}; + export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => { const listKmsKeys = async ( { connectionId, region, destination }: TListAwsConnectionKmsKeys, @@ -82,7 +102,16 @@ export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => return kmsKeys; }; + const listIamUsers = async ({ connectionId }: TListAwsConnectionIamUsers, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor); + + const iamUsers = await listAwsIamUsers(appConnection); + + return iamUsers; + }; + return { - listKmsKeys + listKmsKeys, + listIamUsers }; }; diff --git a/backend/src/services/app-connection/ldap/index.ts b/backend/src/services/app-connection/ldap/index.ts new file mode 100644 index 000000000..639879a08 --- /dev/null +++ b/backend/src/services/app-connection/ldap/index.ts @@ -0,0 +1,4 @@ +export * from "./ldap-connection-enums"; +export * from "./ldap-connection-fns"; +export * from "./ldap-connection-schemas"; +export * from "./ldap-connection-types"; diff --git a/backend/src/services/app-connection/ldap/ldap-connection-enums.ts b/backend/src/services/app-connection/ldap/ldap-connection-enums.ts new file mode 100644 index 000000000..9d6a3d5cc --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-enums.ts @@ -0,0 +1,7 @@ +export enum LdapConnectionMethod { + SimpleBind = "simple-bind" +} + +export enum LdapProvider { + ActiveDirectory = "active-directory" +} diff --git a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts new file mode 100644 index 000000000..03005c7d7 --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts @@ -0,0 +1,102 @@ +import ldap from "ldapjs"; + +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { LdapConnectionMethod } from "./ldap-connection-enums"; +import { TLdapConnectionConfig } from "./ldap-connection-types"; + +export const getLdapConnectionListItem = () => { + return { + name: "LDAP" as const, + app: AppConnection.LDAP as const, + methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind] + }; +}; + +const LDAP_TIMEOUT = 15_000; + +export const getLdapConnectionClient = async ({ + url, + dn, + password, + sslCertificate, + sslRejectUnauthorized = true +}: TLdapConnectionConfig["credentials"]) => { + await blockLocalAndPrivateIpAddresses(url); + + const isSSL = url.startsWith("ldaps"); + + return new Promise((resolve, reject) => { + const client = ldap.createClient({ + url, + timeout: LDAP_TIMEOUT, + connectTimeout: LDAP_TIMEOUT, + tlsOptions: isSSL + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate ? [sslCertificate] : undefined + } + : undefined + }); + + client.on("error", (err: Error) => { + logger.error(err, "LDAP Error"); + client.destroy(); + reject(new Error(`Provider Error - ${err.message}`)); + }); + + client.on("connectError", (err: Error) => { + logger.error(err, "LDAP Connection Error"); + client.destroy(); + reject(new Error(`Provider Connect Error - ${err.message}`)); + }); + + client.on("connectRefused", (err: Error) => { + logger.error(err, "LDAP Connection Refused"); + client.destroy(); + reject(new Error(`Provider Connection Refused - ${err.message}`)); + }); + + client.on("connectTimeout", (err: Error) => { + logger.error(err, "LDAP Connection Timeout"); + client.destroy(); + reject(new Error(`Provider Connection Timeout - ${err.message}`)); + }); + + client.on("connect", () => { + client.bind(dn, password, (err) => { + if (err) { + logger.error(err, "LDAP Bind Error"); + reject(new Error(`Bind Error: ${err.message}`)); + client.destroy(); + } + + resolve(client); + }); + }); + }); +}; + +export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => { + let client: ldap.Client | undefined; + + try { + client = await getLdapConnectionClient(credentials); + + // this shouldn't occur as handle connection error events in client but here as fallback + if (!client.connected) { + throw new BadRequestError({ message: "Unable to connect to LDAP server" }); + } + + return credentials; + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}` + }); + } finally { + client?.destroy(); + } +}; diff --git a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts new file mode 100644 index 000000000..91884b914 --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts @@ -0,0 +1,93 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { DistinguishedNameRegex } from "@app/lib/regex"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums"; + +export const LdapConnectionSimpleBindCredentialsSchema = z.object({ + provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider), + url: z + .string() + .trim() + .min(1, "URL required") + .regex(new RE2(/^ldaps?:\/\//)) + .describe(AppConnections.CREDENTIALS.LDAP.url), + dn: z + .string() + .trim() + .regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") + .min(1, "Distinguished Name (DN) required") + .describe(AppConnections.CREDENTIALS.LDAP.dn), + password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password), + sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + .describe(AppConnections.CREDENTIALS.LDAP.sslCertificate) +}); + +const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.LDAP) +}); + +export const LdapConnectionSchema = z.intersection( + BaseLdapConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(LdapConnectionMethod.SimpleBind), + credentials: LdapConnectionSimpleBindCredentialsSchema + }) + ]) +); + +export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [ + BaseLdapConnectionSchema.extend({ + method: z.literal(LdapConnectionMethod.SimpleBind), + credentials: LdapConnectionSimpleBindCredentialsSchema.pick({ + provider: true, + url: true, + dn: true, + sslRejectUnauthorized: true, + sslCertificate: true + }) + }) +]); + +export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method), + credentials: LdapConnectionSimpleBindCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.LDAP).credentials + ) + }) +]); + +export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP) +); + +export const UpdateLdapConnectionSchema = z + .object({ + credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.LDAP).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP)); + +export const LdapConnectionListItemSchema = z.object({ + name: z.literal("LDAP"), + app: z.literal(AppConnection.LDAP), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), + methods: z.nativeEnum(LdapConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/ldap/ldap-connection-types.ts b/backend/src/services/app-connection/ldap/ldap-connection-types.ts new file mode 100644 index 000000000..ee69b2542 --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-types.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + CreateLdapConnectionSchema, + LdapConnectionSchema, + ValidateLdapConnectionCredentialsSchema +} from "./ldap-connection-schemas"; + +export type TLdapConnection = z.infer; + +export type TLdapConnectionInput = z.infer & { + app: AppConnection.LDAP; +}; + +export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema; + +export type TLdapConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts index 38ef0eef6..994f9a40d 100644 --- a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [ port: true, username: true, sslEnabled: true, - sslRejectUnauthorized: true + sslRejectUnauthorized: true, + sslCertificate: true }) }) ]); diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts index 510f7b7d0..1ddf1e2da 100644 --- a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", port: true, username: true, sslEnabled: true, - sslRejectUnauthorized: true + sslRejectUnauthorized: true, + sslCertificate: true }) }) ]); diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index ed1d99941..bc98e9bcc 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -77,20 +77,22 @@ export const getSqlConnectionClient = async (appConnection: Pick { const { credentials, app } = config; - const client = await getSqlConnectionClient({ app, credentials }); + let client: Knex | undefined; try { + client = await getSqlConnectionClient({ app, credentials }); + await client.raw(`Select 1`); return credentials; } catch (error) { throw new BadRequestError({ - message: - (error as Error)?.message?.replaceAll(credentials.password, "********************") ?? - "Unable to validate connection: verify credentials" + message: `Unable to validate connection: ${ + (error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials" + }` }); } finally { - await client.destroy(); + await client?.destroy(); } }; diff --git a/backend/src/services/app-connection/teamcity/index.ts b/backend/src/services/app-connection/teamcity/index.ts new file mode 100644 index 000000000..89433f440 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/index.ts @@ -0,0 +1,4 @@ +export * from "./teamcity-connection-enums"; +export * from "./teamcity-connection-fns"; +export * from "./teamcity-connection-schemas"; +export * from "./teamcity-connection-types"; diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-enums.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-enums.ts new file mode 100644 index 000000000..7e2f93cb1 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-enums.ts @@ -0,0 +1,3 @@ +export enum TeamCityConnectionMethod { + AccessToken = "access-token" +} diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts new file mode 100644 index 000000000..c87eb06d2 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts @@ -0,0 +1,74 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { TeamCityConnectionMethod } from "./teamcity-connection-enums"; +import { + TTeamCityConnection, + TTeamCityConnectionConfig, + TTeamCityListProjectsResponse +} from "./teamcity-connection-types"; + +export const getTeamCityInstanceUrl = async (config: TTeamCityConnectionConfig) => { + const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + + await blockLocalAndPrivateIpAddresses(instanceUrl); + + return instanceUrl; +}; + +export const getTeamCityConnectionListItem = () => { + return { + name: "TeamCity" as const, + app: AppConnection.TeamCity as const, + methods: Object.values(TeamCityConnectionMethod) as [TeamCityConnectionMethod.AccessToken] + }; +}; + +export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => { + const instanceUrl = await getTeamCityInstanceUrl(config); + + const { accessToken } = config.credentials; + + try { + await request.get(`${instanceUrl}/app/rest/server`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return config.credentials; +}; + +export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => { + const instanceUrl = await getTeamCityInstanceUrl(appConnection); + const { accessToken } = appConnection.credentials; + + const resp = await request.get( + `${instanceUrl}/app/rest/projects?fields=project(id,name,buildTypes(buildType(id,name)))`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + + // Filter out the root project. Should not be seen by users. + return resp.data.project.filter((proj) => proj.id !== "_Root"); +}; diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-schemas.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-schemas.ts new file mode 100644 index 000000000..30494e2ba --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-schemas.ts @@ -0,0 +1,70 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { TeamCityConnectionMethod } from "./teamcity-connection-enums"; + +export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .describe(AppConnections.CREDENTIALS.TEAMCITY.accessToken), + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .min(1, "Instance URL required") + .describe(AppConnections.CREDENTIALS.TEAMCITY.instanceUrl) +}); + +const BaseTeamCityConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.TeamCity) }); + +export const TeamCityConnectionSchema = BaseTeamCityConnectionSchema.extend({ + method: z.literal(TeamCityConnectionMethod.AccessToken), + credentials: TeamCityConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method", [ + BaseTeamCityConnectionSchema.extend({ + method: z.literal(TeamCityConnectionMethod.AccessToken), + credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({ + instanceUrl: true + }) + }) +]); + +export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(TeamCityConnectionMethod.AccessToken) + .describe(AppConnections.CREATE(AppConnection.TeamCity).method), + credentials: TeamCityConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.TeamCity).credentials + ) + }) +]); + +export const CreateTeamCityConnectionSchema = ValidateTeamCityConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.TeamCity) +); + +export const UpdateTeamCityConnectionSchema = z + .object({ + credentials: TeamCityConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.TeamCity).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity)); + +export const TeamCityConnectionListItemSchema = z.object({ + name: z.literal("TeamCity"), + app: z.literal(AppConnection.TeamCity), + methods: z.nativeEnum(TeamCityConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-service.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-service.ts new file mode 100644 index 000000000..afad7f572 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-service.ts @@ -0,0 +1,28 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listTeamCityProjects } from "./teamcity-connection-fns"; +import { TTeamCityConnection } from "./teamcity-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const teamcityConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listProjects = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.TeamCity, connectionId, actor); + + try { + const projects = await listTeamCityProjects(appConnection); + return projects; + } catch (error) { + return []; + } + }; + + return { + listProjects + }; +}; diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-types.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-types.ts new file mode 100644 index 000000000..737e7c70d --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-types.ts @@ -0,0 +1,43 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateTeamCityConnectionSchema, + TeamCityConnectionSchema, + ValidateTeamCityConnectionCredentialsSchema +} from "./teamcity-connection-schemas"; + +export type TTeamCityConnection = z.infer; + +export type TTeamCityConnectionInput = z.infer & { + app: AppConnection.TeamCity; +}; + +export type TValidateTeamCityConnectionCredentialsSchema = typeof ValidateTeamCityConnectionCredentialsSchema; + +export type TTeamCityConnectionConfig = DiscriminativePick< + TTeamCityConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TTeamCityProject = { + id: string; + name: string; +}; + +export type TTeamCityProjectWithBuildTypes = TTeamCityProject & { + buildTypes: { + buildType: { + id: string; + name: string; + }[]; + }; +}; + +export type TTeamCityListProjectsResponse = { + project: TTeamCityProjectWithBuildTypes[]; +}; diff --git a/backend/src/services/auth-token/auth-token-dal.ts b/backend/src/services/auth-token/auth-token-dal.ts index 221b691cf..ca7841d8e 100644 --- a/backend/src/services/auth-token/auth-token-dal.ts +++ b/backend/src/services/auth-token/auth-token-dal.ts @@ -47,7 +47,10 @@ export const tokenDALFactory = (db: TDbClient) => { const findTokenSessions = async (filter: Partial, tx?: Knex) => { try { - const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter); + const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession) + .where(filter) + .orderBy("lastUsed", "desc"); + return sessions; } catch (error) { throw new DatabaseError({ name: "Find all token session", error }); diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 2468e3c8a..f26464340 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -151,6 +151,9 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId }); + const revokeMySessionById = async (userId: string, sessionId: string) => + tokenDAL.deleteTokenSession({ userId, id: sessionId }); + const validateRefreshToken = async (refreshToken?: string) => { const appCfg = getConfig(); if (!refreshToken) @@ -223,6 +226,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu clearTokenSessionById, getTokenSessionByUser, revokeAllMySessions, + revokeMySessionById, validateRefreshToken, fnValidateJwtIdentity, getUserTokenSessionById diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 0e0f999dd..bc9c4afa3 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -3,6 +3,8 @@ import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; @@ -10,7 +12,9 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; +import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; @@ -28,7 +32,14 @@ import { TOauthTokenExchangeDTO, TVerifyMfaTokenDTO } from "./auth-login-type"; -import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type"; +import { + ActorType, + AuthMethod, + AuthModeJwtTokenPayload, + AuthModeMfaJwtTokenPayload, + AuthTokenType, + MfaMethod +} from "./auth-type"; type TAuthLoginServiceFactoryDep = { userDAL: TUserDALFactory; @@ -36,6 +47,7 @@ type TAuthLoginServiceFactoryDep = { tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; totpService: Pick; + auditLogService: Pick; }; export type TAuthLoginFactory = ReturnType; @@ -44,7 +56,8 @@ export const authLoginServiceFactory = ({ tokenService, smtpService, orgDAL, - totpService + totpService, + auditLogService }: TAuthLoginServiceFactoryDep) => { /* * Private @@ -412,6 +425,55 @@ export const authLoginServiceFactory = ({ mfaMethod: decodedToken.mfaMethod }); + // In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced) + if ( + selectedOrg.authEnforced && + selectedOrg.bypassOrgAuthEnabled && + !isAuthMethodSaml(decodedToken.authMethod) && + decodedToken.authMethod !== AuthMethod.OIDC + ) { + await auditLogService.createAuditLog({ + orgId: organizationId, + ipAddress, + userAgent, + userAgentType: getUserAgentType(userAgent), + actor: { + type: ActorType.USER, + metadata: { + email: user.email, + userId: user.id, + username: user.username + } + }, + event: { + type: EventType.ORG_ADMIN_BYPASS_SSO, + metadata: {} + } + }); + + // Notify all admins via email (besides the actor) + const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin); + const adminEmails = orgAdmins + .filter((admin) => admin.user.id !== user.id) + .map((admin) => admin.user.email) + .filter(Boolean) as string[]; + + if (adminEmails.length > 0) { + await smtpService.sendMail({ + recipients: adminEmails, + subjectLine: "Security Alert: Admin SSO Bypass", + substitutions: { + email: user.email, + timestamp: new Date().toISOString(), + ip: ipAddress, + userAgent, + siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com") + }, + template: SmtpTemplates.OrgAdminBreakglassAccess + }); + } + } + return { ...tokens, isMfaEnabled: false diff --git a/backend/src/services/certificate-template/certificate-template-fns.ts b/backend/src/services/certificate-template/certificate-template-fns.ts index fa8055f69..b6ec8d755 100644 --- a/backend/src/services/certificate-template/certificate-template-fns.ts +++ b/backend/src/services/certificate-template/certificate-template-fns.ts @@ -1,3 +1,5 @@ +import RE2 from "re2"; + import { TCertificateTemplates } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; @@ -12,7 +14,7 @@ export const validateCertificateDetailsAgainstTemplate = ( template: TCertificateTemplates ) => { // these are validated in router using validateTemplateRegexField - const commonNameRegex = new RegExp(template.commonName); + const commonNameRegex = new RE2(template.commonName); if (!commonNameRegex.test(cert.commonName)) { throw new BadRequestError({ message: "Invalid common name based on template policy" @@ -25,7 +27,7 @@ export const validateCertificateDetailsAgainstTemplate = ( }); } - const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName); + const subjectAlternativeNameRegex = new RE2(template.subjectAlternativeName); cert.altNames.forEach((altName) => { if (!subjectAlternativeNameRegex.test(altName)) { throw new BadRequestError({ diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 7f0aadfca..fe7b24783 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import axios from "axios"; import jwt from "jsonwebtoken"; +import RE2 from "re2"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -67,6 +68,15 @@ const awsRegionFromHeader = (authorizationHeader: string): string | null => { return null; }; +function isValidAwsRegion(region: string | null): boolean { + const validRegionPattern = new RE2("^[a-z0-9-]+$"); + if (typeof region !== "string" || region.length === 0 || region.length > 20) { + return false; + } + + return validRegionPattern.test(region); +} + export const identityAwsAuthServiceFactory = ({ identityAccessTokenDAL, identityAwsAuthDAL, @@ -84,8 +94,12 @@ export const identityAwsAuthServiceFactory = ({ const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); - const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null; + + if (!isValidAwsRegion(region)) { + throw new BadRequestError({ message: "Invalid AWS region" }); + } + const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint; const { @@ -125,7 +139,7 @@ export const identityAwsAuthServiceFactory = ({ // convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$" // considers exact matches + wildcard matches // heavily validated in router - const regex = new RegExp(`^${principalArn.replaceAll("*", ".*")}$`); + const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`); return regex.test(extractPrincipalArn(Arn)); }); diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts index b3f3ccc94..1a6e5cbd6 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts @@ -1,9 +1,10 @@ +import RE2 from "re2"; import safe from "safe-regex"; import { z } from "zod"; -const twelveDigitRegex = /^\d{12}$/; +const twelveDigitRegex = new RE2(/^\d{12}$/); // akhilmhdh: change this to a normal function later. Checked no redosable at the moment -const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/; +const arnRegex = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/); export const validateAccountIds = z .string() diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index a5e036d35..9c0e8d2dd 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -435,12 +435,16 @@ export const identityKubernetesAuthServiceFactory = ({ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); + if (!identityKubernetesAuth) { + throw new NotFoundError({ message: `Failed to find Kubernetes Auth for identity with ID ${identityId}` }); + } + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ message: "The identity does not have Kubernetes Auth attached" }); } - const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const { permission } = await permissionService.getOrgPermission( actor, diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index 18406f29a..f77becb02 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -50,7 +50,7 @@ const getIntegrationSecretsV2 = async ( } // process secrets in current folder - const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId }); + const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId }); secrets.forEach((secret) => { const secretKey = secret.key; @@ -63,7 +63,6 @@ const getIntegrationSecretsV2 = async ( // if no imports then return secrets in the current folder if (!secretImports.length) return content; const importedSecrets = await fnSecretsV2FromImports({ - projectId: dto.projectId, decryptor: dto.decryptor, folderDAL, secretDAL: secretV2BridgeDAL, diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 6be7397f3..989a5a88c 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -27,6 +27,7 @@ import { randomUUID } from "crypto"; import https from "https"; import sodium from "libsodium-wrappers"; import isEqual from "lodash.isequal"; +import RE2 from "re2"; import { z } from "zod"; import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas"; @@ -4151,7 +4152,7 @@ const syncSecretsWindmill = async ({ ); // eslint-disable-next-line - const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); + const pattern = new RE2("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); for await (const key of Object.keys(secrets)) { if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if (!(key in res)) { diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 07ed90bef..196c18356 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -342,9 +342,12 @@ export const kmsServiceFactory = ({ } return async ({ cipherTextBlob }: Pick) => { - const { data } = await externalKms.decrypt(cipherTextBlob); - - return data; + try { + const { data } = await externalKms.decrypt(cipherTextBlob); + return data; + } finally { + await externalKms.cleanup(); + } }; } @@ -557,9 +560,12 @@ export const kmsServiceFactory = ({ } return async ({ plainText }: Pick) => { - const { encryptedBlob } = await externalKms.encrypt(plainText); - - return { cipherTextBlob: encryptedBlob }; + try { + const { encryptedBlob } = await externalKms.encrypt(plainText); + return { cipherTextBlob: encryptedBlob }; + } finally { + await externalKms.cleanup(); + } }; } @@ -787,13 +793,19 @@ export const kmsServiceFactory = ({ return projectDataKey; } } + } catch (error) { + logger.error( + error, + `getProjectSecretManagerKmsDataKey: Failed to get project data key for [projectId=${projectId}]` + ); + throw error; } finally { await lock?.release(); } } if (!project.kmsSecretManagerEncryptedDataKey) { - throw new Error("Missing project data key"); + throw new BadRequestError({ message: "Missing project data key" }); } const kmsDecryptor = await decryptWithKmsKey({ diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 02bf58321..54b0e1b0f 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + OrgMembershipRole, TableName, TOrganizations, TOrganizationsInsert, @@ -216,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => { try { - const conn = tx || db; + const conn = tx || db.replicaNode(); const members = await conn(TableName.OrgMembership) - // .replicaNode()(TableName.OrgMembership) .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( @@ -251,6 +251,43 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => { + try { + const conn = tx || db.replicaNode(); + const members = await conn(TableName.OrgMembership) + .where(`${TableName.OrgMembership}.orgId`, orgId) + .where(`${TableName.OrgMembership}.role`, role) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + conn.ref("id").withSchema(TableName.OrgMembership), + conn.ref("inviteEmail").withSchema(TableName.OrgMembership), + conn.ref("orgId").withSchema(TableName.OrgMembership), + conn.ref("role").withSchema(TableName.OrgMembership), + conn.ref("roleId").withSchema(TableName.OrgMembership), + conn.ref("status").withSchema(TableName.OrgMembership), + conn.ref("username").withSchema(TableName.Users), + conn.ref("email").withSchema(TableName.Users), + conn.ref("firstName").withSchema(TableName.Users), + conn.ref("lastName").withSchema(TableName.Users), + conn.ref("id").withSchema(TableName.Users).as("userId"), + conn.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: false }); + + return members.map(({ username, email, firstName, lastName, userId, publicKey, ...data }) => ({ + ...data, + user: { username, email, firstName, lastName, id: userId, publicKey } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find org members by role" }); + } + }; + const findOrgGhostUser = async (orgId: string) => { try { const member = await db @@ -472,6 +509,7 @@ export const orgDALFactory = (db: TDbClient) => { findAllOrgsByUserId, ghostUserExists, findOrgMembersByUsername, + findOrgMembersByRole, findOrgGhostUser, create, updateById, diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index 61b703e70..1e71f4605 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -13,7 +13,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => { // special query const findAllProjectMembers = async ( projectId: string, - filter: { usernames?: string[]; username?: string; id?: string } = {} + filter: { usernames?: string[]; username?: string; id?: string; roles?: string[] } = {} ) => { try { const docs = await db @@ -31,6 +31,29 @@ export const projectMembershipDALFactory = (db: TDbClient) => { if (filter.id) { void qb.where(`${TableName.ProjectMembership}.id`, filter.id); } + if (filter.roles && filter.roles.length > 0) { + void qb.whereExists((subQuery) => { + void subQuery + .select("role") + .from(TableName.ProjectUserMembershipRole) + .leftJoin( + TableName.ProjectRoles, + `${TableName.ProjectRoles}.id`, + `${TableName.ProjectUserMembershipRole}.customRoleId` + ) + .whereRaw("??.?? = ??.??", [ + TableName.ProjectUserMembershipRole, + "projectMembershipId", + TableName.ProjectMembership, + "id" + ]) + .where((subQb) => { + void subQb + .whereIn(`${TableName.ProjectUserMembershipRole}.role`, filter.roles as string[]) + .orWhereIn(`${TableName.ProjectRoles}.slug`, filter.roles as string[]); + }); + }); + } }) .join( TableName.UserEncryptionKey, diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 1d6ba969a..1fe4961d2 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -79,7 +79,8 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actorAuthMethod, includeGroupMembers, - projectId + projectId, + roles }: TGetProjectMembershipDTO) => { const { permission } = await permissionService.getProjectPermission({ actor, @@ -91,7 +92,7 @@ export const projectMembershipServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); + const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles }); // projectMembers[0].project if (includeGroupMembers) { diff --git a/backend/src/services/project-membership/project-membership-types.ts b/backend/src/services/project-membership/project-membership-types.ts index 68819f5ae..9b8cf2ac3 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -1,6 +1,6 @@ import { TProjectPermission } from "@app/lib/types"; -export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean } & TProjectPermission; +export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean; roles?: string[] } & TProjectPermission; export type TLeaveProjectDTO = Omit; export enum ProjectUserMembershipTemporaryMode { Relative = "relative" diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 9b7c29c1b..9f2de8a85 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -73,6 +73,7 @@ import { TGetProjectDTO, TGetProjectKmsKey, TGetProjectSlackConfig, + TGetProjectSshConfig, TListProjectAlertsDTO, TListProjectCasDTO, TListProjectCertificateTemplatesDTO, @@ -92,6 +93,7 @@ import { TUpdateProjectKmsDTO, TUpdateProjectNameDTO, TUpdateProjectSlackConfig, + TUpdateProjectSshConfig, TUpdateProjectVersionLimitDTO, TUpgradeProjectDTO } from "./project-types"; @@ -104,7 +106,7 @@ export const DEFAULT_PROJECT_ENVS = [ type TProjectServiceFactoryDep = { projectDAL: TProjectDALFactory; - projectSshConfigDAL: Pick; + projectSshConfigDAL: Pick; projectQueue: TProjectQueueFactory; userDAL: TUserDALFactory; projectBotService: Pick; @@ -129,7 +131,7 @@ type TProjectServiceFactoryDep = { certificateTemplateDAL: Pick; pkiAlertDAL: Pick; pkiCollectionDAL: Pick; - sshCertificateAuthorityDAL: Pick; + sshCertificateAuthorityDAL: Pick; sshCertificateAuthoritySecretDAL: Pick; sshCertificateDAL: Pick; sshCertificateTemplateDAL: Pick; @@ -1327,6 +1329,129 @@ export const projectServiceFactory = ({ return { secretManagerKmsKey: kmsKey }; }; + const getProjectSshConfig = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId + }: TGetProjectSshConfig) => { + const project = await projectDAL.findById(projectId); + if (!project) { + throw new NotFoundError({ + message: `Project with ID '${projectId}' not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + + const projectSshConfig = await projectSshConfigDAL.findOne({ + projectId: project.id + }); + + if (!projectSshConfig) { + throw new NotFoundError({ + message: `Project SSH config with ID '${project.id}' not found` + }); + } + + return projectSshConfig; + }; + + const updateProjectSshConfig = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId, + defaultUserSshCaId, + defaultHostSshCaId + }: TUpdateProjectSshConfig) => { + const project = await projectDAL.findById(projectId); + if (!project) { + throw new NotFoundError({ + message: `Project with ID '${projectId}' not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + + let projectSshConfig = await projectSshConfigDAL.findOne({ + projectId: project.id + }); + + if (!projectSshConfig) { + throw new NotFoundError({ + message: `Project SSH config with ID '${project.id}' not found` + }); + } + + projectSshConfig = await projectSshConfigDAL.transaction(async (tx) => { + if (defaultUserSshCaId) { + const userSshCa = await sshCertificateAuthorityDAL.findOne( + { + id: defaultUserSshCaId, + projectId: project.id + }, + tx + ); + + if (!userSshCa) { + throw new NotFoundError({ + message: "User SSH CA must exist and belong to this project" + }); + } + } + + if (defaultHostSshCaId) { + const hostSshCa = await sshCertificateAuthorityDAL.findOne( + { + id: defaultHostSshCaId, + projectId: project.id + }, + tx + ); + + if (!hostSshCa) { + throw new NotFoundError({ + message: "Host SSH CA must exist and belong to this project" + }); + } + } + + const updatedProjectSshConfig = await projectSshConfigDAL.updateById( + projectSshConfig.id, + { + defaultUserSshCaId, + defaultHostSshCaId + }, + tx + ); + + return updatedProjectSshConfig; + }); + + return projectSshConfig; + }; + const getProjectSlackConfig = async ({ actorId, actor, @@ -1548,6 +1673,8 @@ export const projectServiceFactory = ({ getProjectKmsBackup, loadProjectKmsBackup, getProjectKmsKeys, + getProjectSshConfig, + updateProjectSshConfig, getProjectSlackConfig, updateProjectSlackConfig, requestProjectAccess, diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 444f6309c..274189668 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -159,6 +159,13 @@ export type TListProjectSshCertificatesDTO = { limit: number; } & TProjectPermission; +export type TUpdateProjectSshConfig = { + defaultUserSshCaId?: string; + defaultHostSshCaId?: string; +} & TProjectPermission; + +export type TGetProjectSshConfig = TProjectPermission; + export type TGetProjectSlackConfig = TProjectPermission; export type TUpdateProjectSlackConfig = { diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index 2056d5a2c..c68033911 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -159,8 +159,7 @@ export const fnSecretsV2FromImports = async ({ decryptor, expandSecretReferences, hasSecretAccess, - viewSecretValue, - projectId + viewSecretValue }: { secretImports: (Omit & { importEnv: { id: string; slug: string; name: string }; @@ -177,7 +176,6 @@ export const fnSecretsV2FromImports = async ({ environment: string; }) => Promise; hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; - projectId: string; }) => { const cyclicDetector = new Set(); const stack: { @@ -209,7 +207,10 @@ export const fnSecretsV2FromImports = async ({ ); if (!importedFolders.length) continue; - const importedFolderIds = importedFolders.map((el) => el?.id) as string[]; + const importedFolderIds = importedFolders.filter(Boolean).map((el) => el?.id) as string[]; + + if (!importedFolderIds.length) continue; + const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`); const importedSecrets = await secretDAL.find( @@ -218,8 +219,7 @@ export const fnSecretsV2FromImports = async ({ type: SecretType.Shared }, { - sort: [["id", "asc"]], - useCache: { projectId } + sort: [["id", "asc"]] } ); const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index d21003b10..2015516f5 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -698,7 +698,6 @@ export const secretImportServiceFactory = ({ projectId }); const importedSecrets = await fnSecretsV2FromImports({ - projectId, secretImports, folderDAL, viewSecretValue: true, diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 86273a4ff..687d76f33 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -10,7 +10,8 @@ export enum SecretSync { TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel", - Windmill = "windmill" + Windmill = "windmill", + TeamCity = "teamcity" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 0b821b593..143fa4622 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; +import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill"; @@ -43,7 +44,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION, [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, - [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION + [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, + [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -140,6 +142,8 @@ export const SecretSyncFns = { return VercelSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.TeamCity: + return TeamCitySyncFns.syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -199,6 +203,9 @@ export const SecretSyncFns = { case SecretSync.Windmill: secretMap = await WindmillSyncFns.getSecrets(secretSync); break; + case SecretSync.TeamCity: + secretMap = await TeamCitySyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -252,6 +259,8 @@ export const SecretSyncFns = { return VercelSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.TeamCity: + return TeamCitySyncFns.removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index a9099543d..fdf4dfabb 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -13,7 +13,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.TerraformCloud]: "Terraform Cloud", [SecretSync.Camunda]: "Camunda", [SecretSync.Vercel]: "Vercel", - [SecretSync.Windmill]: "Windmill" + [SecretSync.Windmill]: "Windmill", + [SecretSync.TeamCity]: "TeamCity" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -28,5 +29,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, - [SecretSync.Windmill]: AppConnection.Windmill + [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.TeamCity]: AppConnection.TeamCity }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 9f4e283a9..34ac84947 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -214,7 +214,7 @@ export const secretSyncQueueFactory = ({ canExpandValue: () => true }); - const secrets = await secretV2BridgeDAL.findByFolderId({ folderId, projectId }); + const secrets = await secretV2BridgeDAL.findByFolderId({ folderId }); await Promise.allSettled( secrets.map(async (secret) => { @@ -244,7 +244,6 @@ export const secretSyncQueueFactory = ({ if (secretImports.length) { const importedSecrets = await fnSecretsV2FromImports({ - projectId, decryptor: decryptSecretValue, folderDAL, secretDAL: secretV2BridgeDAL, @@ -357,8 +356,11 @@ export const secretSyncQueueFactory = ({ }; if (Object.hasOwn(secretMap, key)) { - secretsToUpdate.push(secret); - if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData; + // Only update secrets if the source value is not empty + if (value) { + secretsToUpdate.push(secret); + if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData; + } } else { secretsToCreate.push(secret); importedSecretMap[key] = secretData; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 03e92a57a..716c9b44f 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -61,6 +61,12 @@ import { THumanitecSyncListItem, THumanitecSyncWithCredentials } from "./humanitec"; +import { + TTeamCitySync, + TTeamCitySyncInput, + TTeamCitySyncListItem, + TTeamCitySyncWithCredentials +} from "./teamcity/teamcity-sync-types"; import { TTerraformCloudSync, TTerraformCloudSyncInput, @@ -81,7 +87,8 @@ export type TSecretSync = | TTerraformCloudSync | TCamundaSync | TVercelSync - | TWindmillSync; + | TWindmillSync + | TTeamCitySync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -95,7 +102,8 @@ export type TSecretSyncWithCredentials = | TTerraformCloudSyncWithCredentials | TCamundaSyncWithCredentials | TVercelSyncWithCredentials - | TWindmillSyncWithCredentials; + | TWindmillSyncWithCredentials + | TTeamCitySyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -109,7 +117,8 @@ export type TSecretSyncInput = | TTerraformCloudSyncInput | TCamundaSyncInput | TVercelSyncInput - | TWindmillSyncInput; + | TWindmillSyncInput + | TTeamCitySyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -123,7 +132,8 @@ export type TSecretSyncListItem = | TTerraformCloudSyncListItem | TCamundaSyncListItem | TVercelSyncListItem - | TWindmillSyncListItem; + | TWindmillSyncListItem + | TTeamCitySyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/secret-sync/teamcity/index.ts b/backend/src/services/secret-sync/teamcity/index.ts new file mode 100644 index 000000000..add83cb20 --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/index.ts @@ -0,0 +1,4 @@ +export * from "./teamcity-sync-constants"; +export * from "./teamcity-sync-fns"; +export * from "./teamcity-sync-schemas"; +export * from "./teamcity-sync-types"; diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-constants.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-constants.ts new file mode 100644 index 000000000..30f541bbc --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const TEAMCITY_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "TeamCity", + destination: SecretSync.TeamCity, + connection: AppConnection.TeamCity, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts new file mode 100644 index 000000000..323f59851 --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts @@ -0,0 +1,183 @@ +import { request } from "@app/lib/config/request"; +import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; +import { + TDeleteTeamCityVariable, + TPostTeamCityVariable, + TTeamCityListVariables, + TTeamCityListVariablesResponse, + TTeamCitySyncWithCredentials +} from "@app/services/secret-sync/teamcity/teamcity-sync-types"; + +// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env."). +// TeamCity API returns empty string for password-type variables for security reasons. +const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => { + const { data } = await request.get( + buildConfig + ? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters` + : `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + + // Strips out "env." from map key, but the "name" field still has the original unaltered key. + return Object.fromEntries( + data.property.map((variable) => [ + variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name, + { ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security + ]) + ); +}; + +// Create and update both use the same method +const updateTeamCityVariable = async ({ + instanceUrl, + accessToken, + project, + buildConfig, + key, + value +}: TPostTeamCityVariable) => { + return request.post( + buildConfig + ? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters` + : `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`, + { + name: key, + value, + type: { + rawValue: "password display='hidden'" + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); +}; + +const deleteTeamCityVariable = async ({ + instanceUrl, + accessToken, + project, + buildConfig, + key +}: TDeleteTeamCityVariable) => { + return request.delete( + buildConfig + ? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters/${encodeURIComponent(key)}` + : `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters/${encodeURIComponent(key)}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); +}; + +export const TeamCitySyncFns = { + syncSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { project, buildConfig } + } = secretSync; + + const instanceUrl = await getTeamCityInstanceUrl(connection); + const { accessToken } = connection.credentials; + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + const payload = { + instanceUrl, + accessToken, + project, + buildConfig, + key: `env.${key}`, + value + }; + + try { + // Replace every secret since TeamCity does not return secret values that we can cross-check + // No need to differenciate create / update because TeamCity uses the same method for both + await updateTeamCityVariable(payload); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); + + for await (const [key, variable] of Object.entries(variables)) { + if (!(key in secretMap)) { + try { + await deleteTeamCityVariable({ + key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables(). + instanceUrl, + accessToken, + project, + buildConfig + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + }, + removeSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { project, buildConfig } + } = secretSync; + + const instanceUrl = await getTeamCityInstanceUrl(connection); + const { accessToken } = connection.credentials; + + const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); + + for await (const [key, variable] of Object.entries(variables)) { + if (key in secretMap) { + try { + await deleteTeamCityVariable({ + key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables(). + instanceUrl, + accessToken, + project, + buildConfig + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + }, + getSecrets: async (secretSync: TTeamCitySyncWithCredentials) => { + const { + connection, + destinationConfig: { project, buildConfig } + } = secretSync; + + const instanceUrl = await getTeamCityInstanceUrl(connection); + const { accessToken } = connection.credentials; + + return listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); + } +}; diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-schemas.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-schemas.ts new file mode 100644 index 000000000..21c09092f --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-schemas.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const TeamCitySyncDestinationConfigSchema = z.object({ + project: z.string().trim().min(1, "Project required").describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.project), + buildConfig: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.buildConfig) +}); + +const TeamCitySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const TeamCitySyncSchema = BaseSecretSyncSchema(SecretSync.TeamCity, TeamCitySyncOptionsConfig).extend({ + destination: z.literal(SecretSync.TeamCity), + destinationConfig: TeamCitySyncDestinationConfigSchema +}); + +export const CreateTeamCitySyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.TeamCity, + TeamCitySyncOptionsConfig +).extend({ + destinationConfig: TeamCitySyncDestinationConfigSchema +}); + +export const UpdateTeamCitySyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.TeamCity, + TeamCitySyncOptionsConfig +).extend({ + destinationConfig: TeamCitySyncDestinationConfigSchema.optional() +}); + +export const TeamCitySyncListItemSchema = z.object({ + name: z.literal("TeamCity"), + connection: z.literal(AppConnection.TeamCity), + destination: z.literal(SecretSync.TeamCity), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-types.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-types.ts new file mode 100644 index 000000000..8b3f15e0d --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-types.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +import { TTeamCityConnection } from "@app/services/app-connection/teamcity"; + +import { CreateTeamCitySyncSchema, TeamCitySyncListItemSchema, TeamCitySyncSchema } from "./teamcity-sync-schemas"; + +export type TTeamCitySync = z.infer; + +export type TTeamCitySyncInput = z.infer; + +export type TTeamCitySyncListItem = z.infer; + +export type TTeamCitySyncWithCredentials = TTeamCitySync & { + connection: TTeamCityConnection; +}; + +export type TTeamCityVariable = { + name: string; + value: string; + inherited?: boolean; + type: { + rawValue: string; + }; +}; + +export type TTeamCityListVariablesResponse = { + property: (TTeamCityVariable & { value?: string })[]; + count: number; + href: string; +}; + +export type TTeamCityListVariables = { + accessToken: string; + instanceUrl: string; + project: string; + buildConfig?: string; +}; + +export type TPostTeamCityVariable = TTeamCityListVariables & { + key: string; + value: string; +}; + +export type TDeleteTeamCityVariable = TTeamCityListVariables & { + key: string; +}; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index a9c909899..05fc7cd35 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -1,3 +1,4 @@ +import { MongoAbility } from "@casl/ability"; import { Knex } from "knex"; import { validate as uuidValidate } from "uuid"; @@ -15,46 +16,29 @@ import { TFindFilter, TFindOpt } from "@app/lib/knex"; -import { BufferKeysToString, OrderByDirection } from "@app/lib/types"; +import { OrderByDirection } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; -import type { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; +import type { + TFindSecretsByFolderIdsFilter, + TGetSecretsDTO +} from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; -export const SecretDalCacheKeys = { +export const SecretServiceCacheKeys = { get productKey() { const { INFISICAL_PLATFORM_VERSION } = getConfig(); return `${ProjectType.SecretManager}:${INFISICAL_PLATFORM_VERSION || 0}`; }, getSecretDalVersion: (projectId: string) => { - return `${SecretDalCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`; + return `${SecretServiceCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`; }, - findByFolderIds: ( + getSecretsOfServiceLayer: ( projectId: string, version: number, - { useCache, tx, ...cacheKey }: Parameters[0] + dto: TGetSecretsDTO & { permissionRules: MongoAbility["rules"] } ) => { - return `${SecretDalCacheKeys.productKey}:${projectId}:${ + return `${SecretServiceCacheKeys.productKey}:${projectId}:${ TableName.SecretV2 - }-dal:v${version}:find-by-folder-ids:${generateCacheKeyFromData(cacheKey)}`; - }, - findByFolderId: ( - projectId: string, - version: number, - { useCache, tx, ...cacheKey }: Parameters[0] - ) => { - return `${SecretDalCacheKeys.productKey}:${projectId}:${ - TableName.SecretV2 - }-dal:v${version}:find-by-folder-id:${generateCacheKeyFromData(cacheKey)}`; - }, - find: (projectId: string, version: number, ...args: Parameters) => { - const [filter, opts] = args; - delete opts?.tx; - delete opts?.useCache; - return `${SecretDalCacheKeys.productKey}:${projectId}:${ - TableName.SecretV2 - }-dal:v${version}:find:${generateCacheKeyFromData({ - filter, - opts - })}`; + }-dal:v${version}:get-secrets-service-layer:${dto.actorId}-${generateCacheKeyFromData(dto)}`; } }; @@ -64,14 +48,14 @@ interface TSecretV2DalArg { keyStore: TKeyStoreFactory; } -const SECRET_DAL_TTL = 5 * 60; -const SECRET_DAL_VERSION_TTL = 15 * 60; -const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024; +export const SECRET_DAL_TTL = 5 * 60; +export const SECRET_DAL_VERSION_TTL = 15 * 60; +export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024; export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const secretOrm = ormify(db, TableName.SecretV2); const invalidateSecretCacheByProjectId = async (projectId: string) => { - const secretDalVersionKey = SecretDalCacheKeys.getSecretDalVersion(projectId); + const secretDalVersionKey = SecretServiceCacheKeys.getSecretDalVersion(projectId); await keyStore.incrementBy(secretDalVersionKey, 1); await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL); }; @@ -128,35 +112,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } }; - const find = async ( - filter: TFindFilter, - opts: TFindOpt & { useCache?: { projectId: string } } = {} - ) => { - const { offset, limit, sort, tx, useCache } = opts; + const find = async (filter: TFindFilter, opts: TFindOpt = {}) => { + const { offset, limit, sort, tx } = opts; try { - let secretDalVersion = 0; - if (useCache) { - const cachedSecretDalVersion = await keyStore.getItem( - SecretDalCacheKeys.getSecretDalVersion(useCache.projectId) - ); - secretDalVersion = Number(cachedSecretDalVersion || 0); - const cacheKey = SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts); - const cachedSecrets = await keyStore.getItem(cacheKey); - if (cachedSecrets) { - await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL); - - const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[]; - const sanitizedSecrets = unsanitizedSecrets.map((el) => { - const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null; - const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null; - const createdAt = new Date(el.createdAt); - const updatedAt = new Date(el.updatedAt); - return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt }; - }); - return sanitizedSecrets; - } - } - const query = (tx || db)(TableName.SecretV2) // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter)) @@ -225,22 +183,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { ] }); - if (useCache) { - const cachedSecrets = data.map((el) => { - const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null; - const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null; - return { ...el, encryptedValue, encryptedComment }; - }); - const cache = JSON.stringify(cachedSecrets); - if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) { - await keyStore.setItemWithExpiry( - SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts), - SECRET_DAL_TTL, - cache - ); - } - } - return data; } catch (error) { throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` }); @@ -345,15 +287,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } }; - const findByFolderId = async (dto: { - folderId: string; - userId?: string; - tx?: Knex; - projectId: string; - useCache?: boolean; - }) => { + const findByFolderId = async (dto: { folderId: string; userId?: string; tx?: Knex }) => { try { - const { folderId, tx, projectId } = dto; + const { folderId, tx } = dto; let { userId } = dto; // 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)) { @@ -361,27 +297,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { userId = undefined; } - const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId)); - const secretDalVersion = Number(cachedSecretDalVersion || 0); - - if (dto.useCache) { - const cacheKey = SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto); - const cachedSecrets = await keyStore.getItem(cacheKey); - if (cachedSecrets) { - await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL); - - const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[]; - const sanitizedSecrets = unsanitizedSecrets.map((el) => { - const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null; - const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null; - const createdAt = new Date(el.createdAt); - const updatedAt = new Date(el.updatedAt); - return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt }; - }); - return sanitizedSecrets; - } - } - const secs = await (tx || db.replicaNode())(TableName.SecretV2) .where({ folderId }) .where((bd) => { @@ -437,22 +352,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } ] }); - if (dto.useCache) { - const newCachedSecrets = data.map((el) => { - const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null; - const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null; - return { ...el, encryptedValue, encryptedComment }; - }); - const cache = JSON.stringify(newCachedSecrets); - - if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) { - await keyStore.setItemWithExpiry( - SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto), - SECRET_DAL_TTL, - cache - ); - } - } return data; } catch (error) { throw new DatabaseError({ error, name: "get all secret" }); @@ -542,11 +441,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { folderIds: string[]; userId?: string; tx?: Knex; - projectId: string; filters?: TFindSecretsByFolderIdsFilter; - useCache?: boolean; }) => { - const { folderIds, tx, filters, useCache, projectId } = dto; + const { folderIds, tx, filters } = dto; let { userId } = dto; 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) @@ -555,26 +452,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { userId = undefined; } - const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId)); - const secretDalVersion = Number(cachedSecretDalVersion || 0); - if (useCache) { - const cacheKey = SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto); - const cachedSecrets = await keyStore.getItem(cacheKey); - if (cachedSecrets) { - await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL); - - const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[]; - const sanitizedSecrets = unsanitizedSecrets.map((el) => { - const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null; - const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null; - const createdAt = new Date(el.createdAt); - const updatedAt = new Date(el.updatedAt); - return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt }; - }); - return sanitizedSecrets; - } - } - const query = (tx || db.replicaNode())(TableName.SecretV2) .whereIn(`${TableName.SecretV2}.folderId`, folderIds) .where((bd) => { @@ -700,22 +577,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } ] }); - if (useCache) { - const cachedSecrets = data.map((el) => { - const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null; - const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null; - return { ...el, encryptedValue, encryptedComment }; - }); - const cache = JSON.stringify(cachedSecrets); - - if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) { - await keyStore.setItemWithExpiry( - SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto), - SECRET_DAL_TTL, - cache - ); - } - } return data; } catch (error) { diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 3b35ab41a..f42deb8ff 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -1,5 +1,7 @@ import path from "node:path"; +import RE2 from "re2"; + import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -13,9 +15,8 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; -const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g; -// akhilmhdh: JS regex with global save state in .test -const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([a-zA-Z0-9-_.]+)}/; +const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`; +const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING); export const shouldUseSecretV2Bridge = (version: number) => version === 3; @@ -36,7 +37,14 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3; * // ] */ export const getAllSecretReferences = (maybeSecretReference: string) => { - const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]); + const references = []; + let match; + + const regex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + // eslint-disable-next-line no-cond-assign + while ((match = regex.exec(maybeSecretReference)) !== null) { + references.push(match[1]); + } const nestedReferences = references .filter((el) => el.includes(".")) @@ -501,7 +509,7 @@ export const expandSecretReferencesFactory = ({ const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) return { value: "", tags: [] }; - const secrets = await secretDAL.findByFolderId({ folderId: folder.id, projectId, useCache: true }); + const secrets = await secretDAL.findByFolderId({ folderId: folder.id }); const decryptedSecret = secrets.reduce>((prev, secret) => { // eslint-disable-next-line no-param-reassign @@ -531,9 +539,17 @@ export const expandSecretReferencesFactory = ({ // eslint-disable-next-line no-continue if (depth > MAX_SECRET_REFERENCE_DEPTH) continue; - const refs = value?.match(INTERPOLATION_SYNTAX_REG); - if (refs) { + const matchRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + const refs = []; + let match; + + // eslint-disable-next-line no-cond-assign + while ((match = matchRegex.exec(value || "")) !== null) { + refs.push(match[0]); + } + + if (refs.length > 0) { for (const interpolationSyntax of refs) { const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); const entities = interpolationKey.trim().split("."); @@ -592,7 +608,7 @@ export const expandSecretReferencesFactory = ({ trace }; - const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue); + const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue); if (dto.shouldStackTrace) { const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null }; trace?.children.push(stackTraceNode); @@ -626,7 +642,7 @@ export const expandSecretReferencesFactory = ({ }) => { if (!inputSecret.value) return inputSecret.value; - const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG)); + const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value); if (!shouldExpand) return inputSecret.value; const { expandedValue } = await recursivelyExpandSecret(inputSecret); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 596ebb5a1..ca815c6e1 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -25,6 +25,7 @@ import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-app import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { diff, groupBy } from "@app/lib/fn"; @@ -43,7 +44,12 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; -import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; +import { + MAX_SECRET_CACHE_BYTES, + SECRET_DAL_TTL, + SecretServiceCacheKeys, + TSecretV2BridgeDALFactory +} from "./secret-v2-bridge-dal"; import { buildHierarchy, expandSecretReferencesFactory, @@ -105,6 +111,7 @@ type TSecretV2BridgeServiceFactoryDep = { >; snapshotService: Pick; resourceMetadataDAL: Pick; + keyStore: Pick; }; export type TSecretV2BridgeServiceFactory = ReturnType; @@ -127,7 +134,8 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalRequestDAL, secretApprovalRequestSecretDAL, kmsService, - resourceMetadataDAL + resourceMetadataDAL, + keyStore }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, @@ -800,12 +808,10 @@ export const secretV2BridgeServiceFactory = ({ const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); const secrets = await secretDAL.findByFolderIds({ - projectId, folderIds: folderMappings.map((folderMapping) => folderMapping.folderId), userId, tx: undefined, - filters, - useCache: true + filters }); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ @@ -909,21 +915,22 @@ export const secretV2BridgeServiceFactory = ({ return decryptedSecrets; }; - const getSecrets = async ({ - actorId, - path, - environment, - projectId, - actor, - actorOrgId, - viewSecretValue, - actorAuthMethod, - includeImports, - recursive, - expandSecretReferences: shouldExpandSecretReferences, - throwOnMissingReadValuePermission = true, - ...params - }: TGetSecretsDTO) => { + const getSecrets = async (dto: TGetSecretsDTO) => { + const { + actorId, + path, + environment, + projectId, + actor, + actorOrgId, + viewSecretValue, + actorAuthMethod, + includeImports, + recursive, + expandSecretReferences: shouldExpandSecretReferences, + throwOnMissingReadValuePermission = true, + ...params + } = dto; const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -934,6 +941,42 @@ export const secretV2BridgeServiceFactory = ({ }); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); + const cachedSecretDalVersion = await keyStore.getItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); + const secretDalVersion = Number(cachedSecretDalVersion || 0); + const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, { + ...dto, + permissionRules: permission.rules + }); + + const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const encryptedCachedSecrets = await keyStore.getItem(cacheKey); + if (encryptedCachedSecrets) { + try { + await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL); + const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") }); + const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as { + secrets: typeof decryptedSecrets; + imports: typeof importedSecrets; + }; + return { + secrets: secrets.map((el) => ({ + ...el, + createdAt: new Date(el.createdAt), + updatedAt: new Date(el.updatedAt) + })), + imports + }; + } catch (err) { + logger.error(err, "Secret service layer cache miss"); + await keyStore.deleteItem(cacheKey); + } + } + let paths: { folderId: string; path: string }[] = []; if (recursive) { @@ -958,17 +1001,10 @@ export const secretV2BridgeServiceFactory = ({ const groupedPaths = groupBy(paths, (p) => p.folderId); const secrets = await secretDAL.findByFolderIds({ - projectId, folderIds: paths.map((p) => p.folderId), userId: actorId, tx: undefined, - filters: params, - useCache: true - }); - - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId + filters: params }); // scott: if any of this changes it also needs to be mirrored in secret rotation for getting dashboard secrets @@ -1086,15 +1122,19 @@ export const secretV2BridgeServiceFactory = ({ } if (!includeImports) { - return { - secrets: decryptedSecrets - }; + const payload = { secrets: decryptedSecrets, imports: [] }; + const encryptedUpdatedCachedSecrets = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(payload)) + }).cipherTextBlob; + if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) { + await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64")); + } + return payload; } const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ - projectId, viewSecretValue, secretImports: allowedImports, secretDAL, @@ -1129,10 +1169,14 @@ export const secretV2BridgeServiceFactory = ({ } }); - return { - secrets: decryptedSecrets, - imports: importedSecrets - }; + const payload = { secrets: decryptedSecrets, imports: importedSecrets }; + const encryptedUpdatedCachedSecrets = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(payload)) + }).cipherTextBlob; + if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) { + await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64")); + } + return payload; }; const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => { @@ -1312,7 +1356,6 @@ export const secretV2BridgeServiceFactory = ({ if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ - projectId, secretImports, viewSecretValue, secretDAL, @@ -2729,7 +2772,7 @@ export const secretV2BridgeServiceFactory = ({ generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)]) ); - const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id), projectId, useCache: true }); + const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id) }); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 2574dc13e..f08a5a04c 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,5 +1,6 @@ /* eslint-disable no-await-in-loop */ import path from "path"; +import RE2 from "re2"; import { ActionProjectType, @@ -218,7 +219,9 @@ type TInterpolateSecretArg = { }; const MAX_SECRET_REFERENCE_DEPTH = 5; -const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g; +const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`; +const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING); + export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => { const secretCache: Record> = {}; const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; @@ -273,9 +276,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD if (!value) return ""; if (depth > MAX_SECRET_REFERENCE_DEPTH) return ""; - const refs = value.match(INTERPOLATION_SYNTAX_REG); + const refs = []; + let match; + const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + + // eslint-disable-next-line no-cond-assign + while ((match = execRegex.exec(value)) !== null) { + refs.push(match[0]); + } + let expandedValue = value; - if (refs) { + if (refs.length > 0) { for (const interpolationSyntax of refs) { const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); const entities = interpolationKey.trim().split("."); @@ -284,7 +295,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD const [secretKey] = entities; // eslint-disable-next-line let referenceValue = await fetchSecret(environment, secretPath, secretKey); - if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) { + if (INTERPOLATION_TEST_REGEX.test(referenceValue)) { // eslint-disable-next-line referenceValue = await recursivelyExpandSecret({ environment, @@ -305,7 +316,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD // eslint-disable-next-line let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); - if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) { + if (INTERPOLATION_TEST_REGEX.test(referenceValue)) { // eslint-disable-next-line referenceValue = await recursivelyExpandSecret({ environment: secretReferenceEnvironment, @@ -332,7 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD }) => { if (!inputSecret.value) return inputSecret.value; - const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG)); + const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value); if (!shouldExpand) return inputSecret.value; const expandedSecretValue = await recursivelyExpandSecret(inputSecret); @@ -451,7 +462,18 @@ export const fnSecretBlindIndexCheckV2 = async ({ * // ] */ export const getAllNestedSecretReferences = (maybeSecretReference: string) => { - const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]); + const matches = []; + let match; + + const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + + // eslint-disable-next-line no-cond-assign + while ((match = execRegex.exec(maybeSecretReference)) !== null) { + matches.push(match); + } + + const references = matches.map((m) => m[1]); + return references .filter((el) => el.includes(".")) .map((el) => { diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 0d36250fb..5791c415d 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -367,7 +367,7 @@ export const secretQueueFactory = ({ canExpandValue: () => true }); // process secrets in current folder - const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId }); + const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId }); await Promise.allSettled( secrets.map(async (secret) => { @@ -397,7 +397,6 @@ export const secretQueueFactory = ({ // if no imports then return secrets in the current folder if (!secretImports.length) return content; const importedSecrets = await fnSecretsV2FromImports({ - projectId: dto.projectId, decryptor: dto.decryptor, folderDAL, secretDAL: secretV2BridgeDAL, diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 25f5f3949..550e1bb07 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -44,6 +44,7 @@ export enum SmtpTemplates { SecretRotationFailed = "secretRotationFailed.handlebars", ProjectAccessRequest = "projectAccess.handlebars", OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars", + OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars", ServiceTokenExpired = "serviceTokenExpired.handlebars" } diff --git a/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars b/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars new file mode 100644 index 000000000..cc97ff201 --- /dev/null +++ b/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars @@ -0,0 +1,20 @@ + + + + + + Organization admin has bypassed SSO + + + +

Infisical

+

The organization admin {{email}} has bypassed enforced SSO login.

+

Timestamp: {{timestamp}}

+

IP address: {{ip}}

+

User agent: {{userAgent}}

+

If you'd like to disable Admin SSO Bypass, please visit Organization Settings > Security.

+ + {{emailFooter}} + + + diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go index 5b2bb37bb..a11e4da4c 100644 --- a/cli/packages/cmd/ssh.go +++ b/cli/packages/cmd/ssh.go @@ -177,7 +177,6 @@ func issueCredentials(cmd *cobra.Command, args []string) { infisicalToken = token.Token } else { util.RequireLogin() - util.RequireLocalWorkspaceFile() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { @@ -411,7 +410,6 @@ func signKey(cmd *cobra.Command, args []string) { infisicalToken = token.Token } else { util.RequireLogin() - util.RequireLocalWorkspaceFile() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { @@ -610,25 +608,82 @@ func signKey(cmd *cobra.Command, args []string) { } func sshConnect(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + token, err := util.GetInfisicalToken(cmd) if err != nil { - util.HandleError(err, "Unable to authenticate") + util.HandleError(err, "Unable to parse flag") } + + var infisicalToken string - if loggedInUserDetails.LoginExpired { - util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } - infisicalToken := loggedInUserDetails.UserCredentials.JTWToken - writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile") if err != nil { util.HandleError(err, "Unable to parse --writeHostCaToFile flag") } + outFilePath, err := cmd.Flags().GetString("outFilePath") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + hostname, _ := cmd.Flags().GetString("hostname") + loginUser, _ := cmd.Flags().GetString("loginUser") + + var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string + if outFilePath != "" { + if strings.HasPrefix(outFilePath, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + util.HandleError(err, "Failed to resolve home directory") + } + outFilePath = strings.Replace(outFilePath, "~", homeDir, 1) + } + + if strings.HasSuffix(outFilePath, "-cert.pub") { + signedKeyPath = outFilePath + baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub") + outputDir = filepath.Dir(outFilePath) + privateKeyPath = filepath.Join(outputDir, baseName) + publicKeyPath = filepath.Join(outputDir, baseName+".pub") + } else { + outputDir = outFilePath + info, err := os.Stat(outputDir) + if os.IsNotExist(err) { + err = os.MkdirAll(outputDir, 0755) + if err != nil { + util.HandleError(err, "Failed to create output directory") + } + } else if err != nil { + util.HandleError(err, "Failed to access output directory") + } else if !info.IsDir() { + util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory") + } + fileName := "id_ed25519" + privateKeyPath = filepath.Join(outputDir, fileName) + publicKeyPath = filepath.Join(outputDir, fileName+".pub") + signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") + } + + if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" { + util.PrintErrorMessageAndExit("Failed to resolve file paths for writing credentials") + } + } + customHeaders, err := util.GetInfisicalCustomHeadersMap() if err != nil { util.HandleError(err, "Unable to get custom headers") @@ -651,43 +706,68 @@ func sshConnect(cmd *cobra.Command, args []string) { util.PrintErrorMessageAndExit("You do not have access to any SSH hosts") } - // Prompt to select host - hostNames := make([]string, len(hosts)) - for i, h := range hosts { - hostNames[i] = h.Hostname + var selectedHost = hosts[0] + if hostname != "" { + foundHost := false + for _, h := range hosts { + if h.Hostname == hostname { + selectedHost = h + foundHost = true + break + } + } + if !foundHost { + util.PrintErrorMessageAndExit("Specified --hostname not found or not accessible") + } + } else { + hostNames := make([]string, len(hosts)) + for i, h := range hosts { + hostNames[i] = h.Hostname + } + hostPrompt := promptui.Select{ + Label: "Select an SSH Host", + Items: hostNames, + Size: 10, + } + hostIdx, _, err := hostPrompt.Run() + if err != nil { + util.HandleError(err, "Prompt failed") + } + selectedHost = hosts[hostIdx] } - hostPrompt := promptui.Select{ - Label: "Select an SSH Host", - Items: hostNames, - Size: 10, + var selectedLoginUser string + if loginUser != "" { + foundLoginUser := false + for _, m := range selectedHost.LoginMappings { + if m.LoginUser == loginUser { + selectedLoginUser = loginUser + foundLoginUser = true + break + } + } + if !foundLoginUser { + util.PrintErrorMessageAndExit("Specified --loginUser not valid for selected host") + } + } else { + if len(selectedHost.LoginMappings) == 0 { + util.PrintErrorMessageAndExit("No login users available for selected host") + } + loginUsers := make([]string, len(selectedHost.LoginMappings)) + for i, m := range selectedHost.LoginMappings { + loginUsers[i] = m.LoginUser + } + loginPrompt := promptui.Select{ + Label: "Select Login User", + Items: loginUsers, + Size: 5, + } + loginIdx, _, err := loginPrompt.Run() + if err != nil { + util.HandleError(err, "Prompt failed") + } + selectedLoginUser = selectedHost.LoginMappings[loginIdx].LoginUser } - hostIdx, _, err := hostPrompt.Run() - if err != nil { - util.HandleError(err, "Prompt failed") - } - selectedHost := hosts[hostIdx] - - // Prompt to select login user - if len(selectedHost.LoginMappings) == 0 { - util.PrintErrorMessageAndExit("No login users available for selected host") - } - - loginUsers := make([]string, len(selectedHost.LoginMappings)) - for i, m := range selectedHost.LoginMappings { - loginUsers[i] = m.LoginUser - } - - loginPrompt := promptui.Select{ - Label: "Select Login User", - Items: loginUsers, - Size: 5, - } - loginIdx, _, err := loginPrompt.Run() - if err != nil { - util.HandleError(err, "Prompt failed") - } - selectedLoginUser := selectedHost.LoginMappings[loginIdx].LoginUser // Issue SSH creds for host creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{ @@ -731,10 +811,27 @@ func sshConnect(cmd *cobra.Command, args []string) { util.HandleError(err, "Failed to write Host CA to known_hosts") } - fmt.Printf("📁 Wrote Host CA entry to %s\n", knownHostsPath) + fmt.Printf("Successfully wrote Host CA entry to %s\n", knownHostsPath) } } + if outFilePath != "" { + err = writeToFile(privateKeyPath, creds.PrivateKey, 0600) + if err != nil { + util.HandleError(err, "Failed to write private key") + } + err = writeToFile(publicKeyPath, creds.PublicKey, 0644) + if err != nil { + util.HandleError(err, "Failed to write public key") + } + err = writeToFile(signedKeyPath, creds.SignedKey, 0644) + if err != nil { + util.HandleError(err, "Failed to write signed cert") + } + fmt.Printf("Successfully wrote credentials to %s, %s, and %s\n", privateKeyPath, publicKeyPath, signedKeyPath) + return + } + // Load credentials into SSH agent err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey) if err != nil { @@ -769,7 +866,6 @@ func sshAddHost(cmd *cobra.Command, args []string) { infisicalToken = token.Token } else { util.RequireLogin() - util.RequireLocalWorkspaceFile() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { @@ -1006,16 +1102,20 @@ func init() { sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent") sshCmd.AddCommand(sshIssueCredentialsCmd) - sshConnectCmd.Flags().Bool("writeHostCaToFile", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist") + sshConnectCmd.Flags().String("token", "", "Use a machine identity access token") + sshConnectCmd.Flags().Bool("write-host-ca-to-file", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist") + sshConnectCmd.Flags().String("hostname", "", "Hostname of the SSH host to connect to") + sshConnectCmd.Flags().String("login-user", "", "Login user for the SSH connection") + sshConnectCmd.Flags().String("out-file-path", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be added to the SSH agent and used to establish an interactive SSH connection") sshCmd.AddCommand(sshConnectCmd) sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token") sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)") sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)") - sshAddHostCmd.Flags().Bool("writeUserCaToFile", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub") - sshAddHostCmd.Flags().String("userCaOutFilePath", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key") - sshAddHostCmd.Flags().Bool("writeHostCertToFile", false, "Write SSH host certificate to /etc/ssh/ssh_host__key-cert.pub") - sshAddHostCmd.Flags().Bool("configureSshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file") + sshAddHostCmd.Flags().Bool("write-user-ca-to-file", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub") + sshAddHostCmd.Flags().String("user-ca-out-file-path", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key") + sshAddHostCmd.Flags().Bool("write-host-cert-to-file", false, "Write SSH host certificate to /etc/ssh/ssh_host__key-cert.pub") + sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file") sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile") sshCmd.AddCommand(sshAddHostCmd) diff --git a/cli/packages/cmd/user.go b/cli/packages/cmd/user.go index d3e6096a9..2879e4ccb 100644 --- a/cli/packages/cmd/user.go +++ b/cli/packages/cmd/user.go @@ -1,9 +1,12 @@ package cmd import ( + "encoding/base64" + "encoding/json" "errors" "fmt" "net/url" + "strings" "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" @@ -85,6 +88,57 @@ var switchCmd = &cobra.Command{ }, } +var userGetCmd = &cobra.Command{ + Use: "get", + Short: "Used to get properties of an Infisical profile", + DisableFlagsInUseLine: true, + Example: "infisical user get", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +var userGetTokenCmd = &cobra.Command{ + Use: "token", + Short: "Used to get the access token of an Infisical user", + DisableFlagsInUseLine: true, + Example: "infisical user get token", + Args: cobra.ExactArgs(0), + PreRun: func(cmd *cobra.Command, args []string) { + util.RequireLogin() + }, + Run: func(cmd *cobra.Command, args []string) { + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + if err != nil { + util.HandleError(err, "[infisical user get token]: Unable to get logged in user token") + } + + tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JTWToken, ".") + if len(tokenParts) != 3 { + util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format") + } + + payload, err := base64.RawURLEncoding.DecodeString(tokenParts[1]) + if err != nil { + util.HandleError(err, "[infisical user get token]: Unable to decode token payload") + } + + var tokenPayload struct { + TokenVersionId string `json:"tokenVersionId"` + } + if err := json.Unmarshal(payload, &tokenPayload); err != nil { + util.HandleError(err, "[infisical user get token]: Unable to parse token payload") + } + + fmt.Println("Session ID:", tokenPayload.TokenVersionId) + fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken) + }, +} + var updateCmd = &cobra.Command{ Use: "update", Short: "Used to update properties of an Infisical profile", @@ -185,6 +239,8 @@ var domainCmd = &cobra.Command{ func init() { updateCmd.AddCommand(domainCmd) userCmd.AddCommand(updateCmd) + userGetCmd.AddCommand(userGetTokenCmd) + userCmd.AddCommand(userGetCmd) userCmd.AddCommand(switchCmd) rootCmd.AddCommand(userCmd) } diff --git a/docs/api-reference/endpoints/app-connections/ldap/available.mdx b/docs/api-reference/endpoints/app-connections/ldap/available.mdx new file mode 100644 index 000000000..b42f2bc3d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/ldap/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/create.mdx b/docs/api-reference/endpoints/app-connections/ldap/create.mdx new file mode 100644 index 000000000..181a76902 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/ldap" +--- + + + Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/ldap/delete.mdx b/docs/api-reference/endpoints/app-connections/ldap/delete.mdx new file mode 100644 index 000000000..4888fd04d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/ldap/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx new file mode 100644 index 000000000..7c4524ed8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/ldap/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx new file mode 100644 index 000000000..dc7516bba --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/ldap/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/list.mdx b/docs/api-reference/endpoints/app-connections/ldap/list.mdx new file mode 100644 index 000000000..e909c9266 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/ldap" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/update.mdx b/docs/api-reference/endpoints/app-connections/ldap/update.mdx new file mode 100644 index 000000000..06c7f7f77 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/ldap/{connectionId}" +--- + + + Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/teamcity/available.mdx b/docs/api-reference/endpoints/app-connections/teamcity/available.mdx new file mode 100644 index 000000000..c5cbd3c9d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/teamcity/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/create.mdx b/docs/api-reference/endpoints/app-connections/teamcity/create.mdx new file mode 100644 index 000000000..19d30af70 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/teamcity" +--- + + + Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain + the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/teamcity/delete.mdx b/docs/api-reference/endpoints/app-connections/teamcity/delete.mdx new file mode 100644 index 000000000..d4a5d67ae --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/teamcity/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/teamcity/get-by-id.mdx new file mode 100644 index 000000000..090725826 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/teamcity/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/teamcity/get-by-name.mdx new file mode 100644 index 000000000..ccb46a27d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/teamcity/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/list.mdx b/docs/api-reference/endpoints/app-connections/teamcity/list.mdx new file mode 100644 index 000000000..e4987a876 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/teamcity" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/update.mdx b/docs/api-reference/endpoints/app-connections/teamcity/update.mdx new file mode 100644 index 000000000..499f4a379 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/teamcity/{connectionId}" +--- + + + Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain + the required credentials. + diff --git a/docs/api-reference/endpoints/project-groups/create.mdx b/docs/api-reference/endpoints/project-groups/create.mdx index 6b468085e..6dd7f1a4f 100644 --- a/docs/api-reference/endpoints/project-groups/create.mdx +++ b/docs/api-reference/endpoints/project-groups/create.mdx @@ -1,4 +1,4 @@ --- title: "Create Project Membership" -openapi: "POST /api/v2/workspace/{projectId}/groups/{groupId}" +openapi: "POST /api/v2/workspace/{projectId}/groups/{groupIdOrName}" --- diff --git a/docs/api-reference/endpoints/project-roles/create.mdx b/docs/api-reference/endpoints/project-roles/create.mdx index 7ebfff262..97570ec36 100644 --- a/docs/api-reference/endpoints/project-roles/create.mdx +++ b/docs/api-reference/endpoints/project-roles/create.mdx @@ -1,8 +1,10 @@ --- title: "Create" -openapi: "POST /api/v1/workspace/{projectSlug}/roles" +openapi: "POST /api/v2/workspace/{projectId}/roles" --- - You can read more about the permissions field in the [permissions documentation](/internals/permissions). - \ No newline at end of file + You can read more about the permissions field in the [permissions + documentation](/internals/permissions). + + diff --git a/docs/api-reference/endpoints/project-roles/delete.mdx b/docs/api-reference/endpoints/project-roles/delete.mdx index 6362c2154..41edfa7c3 100644 --- a/docs/api-reference/endpoints/project-roles/delete.mdx +++ b/docs/api-reference/endpoints/project-roles/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/workspace/{projectSlug}/roles/{roleId}" +openapi: "DELETE /api/v2/workspace/{projectId}/roles/{roleId}" --- diff --git a/docs/api-reference/endpoints/project-roles/get-by-slug.mdx b/docs/api-reference/endpoints/project-roles/get-by-slug.mdx index 18817bca9..dfc5c582a 100644 --- a/docs/api-reference/endpoints/project-roles/get-by-slug.mdx +++ b/docs/api-reference/endpoints/project-roles/get-by-slug.mdx @@ -1,4 +1,4 @@ --- title: "Get By Slug" -openapi: "GET /api/v1/workspace/{projectSlug}/roles/slug/{slug}" +openapi: "GET /api/v2/workspace/{projectId}/roles/slug/{roleSlug}" --- diff --git a/docs/api-reference/endpoints/project-roles/list.mdx b/docs/api-reference/endpoints/project-roles/list.mdx index ca83d6e7d..8d8dc10c1 100644 --- a/docs/api-reference/endpoints/project-roles/list.mdx +++ b/docs/api-reference/endpoints/project-roles/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/workspace/{projectSlug}/roles" +openapi: "GET /api/v2/workspace/{projectId}/roles" --- diff --git a/docs/api-reference/endpoints/project-roles/update.mdx b/docs/api-reference/endpoints/project-roles/update.mdx index 5a3d9668e..662d5e617 100644 --- a/docs/api-reference/endpoints/project-roles/update.mdx +++ b/docs/api-reference/endpoints/project-roles/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/workspace/{projectSlug}/roles/{roleId}" +openapi: "PATCH /api/v2/workspace/{projectId}/roles/{roleId}" --- diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/create.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/create.mdx new file mode 100644 index 000000000..69557bb80 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/aws-iam-user-secret" +--- + + + Check out the configuration docs for [AWS IAM User Secret Rotations](/documentation/platform/secret-rotation/aws-iam-user-secret) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete.mdx new file mode 100644 index 000000000..457e35b42 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id.mdx new file mode 100644 index 000000000..3e71aa4d7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name.mdx new file mode 100644 index 000000000..ccc4b9e28 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..0ade7a3d9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/list.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/list.mdx new file mode 100644 index 000000000..6776b2d0f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets.mdx new file mode 100644 index 000000000..6eda840d4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/update.mdx b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/update.mdx new file mode 100644 index 000000000..e276af8d9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/aws-iam-user-secret/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}" +--- + + + Check out the configuration docs for [AWS IAM User Secret Rotations](/documentation/platform/secret-rotation/aws-iam-user-secret) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx new file mode 100644 index 000000000..682b531ad --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/ldap-password" +--- + + + Check out the configuration docs for [LDAP Password Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx new file mode 100644 index 000000000..d4cee951f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/ldap-password/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx new file mode 100644 index 000000000..f422d036d --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx new file mode 100644 index 000000000..68de6a722 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/ldap-password/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..6aed49218 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx new file mode 100644 index 000000000..bf2bb5562 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/ldap-password" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx new file mode 100644 index 000000000..8ad2ae52b --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/ldap-password/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx new file mode 100644 index 000000000..b59ea5250 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/ldap-password/{rotationId}" +--- + + + Check out the configuration docs for [LDAP Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/create.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/create.mdx new file mode 100644 index 000000000..438702b34 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/teamcity" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/delete.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/delete.mdx new file mode 100644 index 000000000..f2b1af6eb --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/teamcity/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-id.mdx new file mode 100644 index 000000000..857d1d5a2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/teamcity/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-name.mdx new file mode 100644 index 000000000..e7101c7b7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/teamcity/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/import-secrets.mdx new file mode 100644 index 000000000..961259300 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/list.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/list.mdx new file mode 100644 index 000000000..d3a6a8373 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/teamcity" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/remove-secrets.mdx new file mode 100644 index 000000000..0f63752ba --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/sync-secrets.mdx new file mode 100644 index 000000000..36eaa361c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/update.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/update.mdx new file mode 100644 index 000000000..820c81b21 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/teamcity/{syncId}" +--- diff --git a/docs/cli/commands/ssh.mdx b/docs/cli/commands/ssh.mdx index 78712ba6f..d99a69dda 100644 --- a/docs/cli/commands/ssh.mdx +++ b/docs/cli/commands/ssh.mdx @@ -7,10 +7,38 @@ description: "Generate SSH credentials with the CLI" [Infisical SSH](/documentation/platform/ssh) lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure. -This command enables you to obtain SSH credentials used to access a remote host; we recommend using the `issue-credentials` sub-command to generate dynamic SSH credentials for each SSH session. +This command enables you to obtain SSH credentials used to access a remote host. We recommend using the `connect` sub-command which handles the full workflow of issuing credentials and establishing an SSH connection in one step. ### Sub-commands + + This command is used to connect to an SSH host using issued credentials. It will automatically issue credentials and either add them to your SSH agent or write them to disk before establishing an SSH connection. + + ```bash + $ infisical ssh connect + ``` + + ### Flags + + The hostname of the SSH host to connect to. If not provided, you will be prompted to select from available hosts. + + + The login user for the SSH connection. If not provided, you will be prompted to select from available login users. + + + Whether to write the Host CA public key to `~/.ssh/known_hosts` if it doesn't already exist. + + Default value: `true` + + + The path to write the SSH credentials to such as `~/.ssh`, `./some_folder`, `./some_folder/id_rsa-cert.pub`. If not provided, the credentials will be added to the SSH agent and used to establish an interactive SSH connection. + + + An authenticated token to use to authenticate with Infisical. + + + + This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template. @@ -29,43 +57,44 @@ This command enables you to obtain SSH credentials used to access a remote host; Whether to add issued SSH credentials to the SSH agent. - + Default value: `false` - + Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully. The path to write the SSH credentials to such as `~/.ssh`, `./some_folder`, `./some_folder/id_rsa-cert.pub`. If not provided, the credentials will be saved to the current working directory where the command is run. - + Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully. The key algorithm to issue SSH credentials for. - + Default value: `RSA_2048` - + Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`. The certificate type to issue SSH credentials for. - + Default value: `user` - + Available options: `user` or `host` The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`). - + Defaults to the Default TTL value set in the certificate template. A custom Key ID to issue SSH credentials for. - + Defaults to the autogenerated Key ID by Infisical. An authenticated token to use to issue SSH credentials. + @@ -95,22 +124,23 @@ This command enables you to obtain SSH credentials used to access a remote host; The certificate type to issue SSH credentials for. - + Default value: `user` - + Available options: `user` or `host` The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`). - + Defaults to the Default TTL value set in the certificate template. A custom Key ID to issue SSH credentials for. - + Defaults to the autogenerated Key ID by Infisical. An authenticated token to use to issue SSH credentials. - \ No newline at end of file + + diff --git a/docs/cli/commands/user.mdx b/docs/cli/commands/user.mdx index 38a92bbab..43a6111e1 100644 --- a/docs/cli/commands/user.mdx +++ b/docs/cli/commands/user.mdx @@ -8,22 +8,46 @@ infisical user ``` ## Description + This command allows you to manage the current logged in users on the CLI -### Sub-commands +### Sub-commands + - Use this command to switch between profiles that are currently logged into the CLI + Use this command to switch between profiles that are currently logged into the CLI + +```bash +infisical user switch +``` - ```bash - infisical user switch - ``` With this command, you can modify the backend API that is utilized for all requests associated with a specific profile. For instance, you have the option to point the profile to use either the Infisical Cloud or your own self-hosted Infisical instance. - ```bash - infisical user update domain +```bash +infisical user update domain +``` + + + + + Use this command to get your current Infisical access token and session information. This command requires you to be logged in. + + The command will display: + + - Your session ID + - Your full JWT access token + + ```bash + infisical user get token + ``` + + Example output: + + ```bash + Session ID: abc123-xyz-456 + Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 89824e9f7..e0cbbe387 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -28,21 +28,32 @@ You can use it across various environments, whether it's local development, CI/C ``` - Use [Scoop](https://scoop.sh/) package manager - ```bash - scoop bucket add org https://github.com/Infisical/scoop-infisical.git - ``` + + Use [Scoop](https://scoop.sh/) package manager - ```bash - scoop install infisical - ``` + ```bash + scoop bucket add org https://github.com/Infisical/scoop-infisical.git + ``` - ### Updates + ```bash + scoop install infisical + ``` - ```bash - scoop update infisical - ``` + ### Updates + + ```bash + scoop update infisical + ``` + + + + Use [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) package manager + + ```bash + winget install infisical + ``` + diff --git a/docs/documentation/guides/organization-structure.mdx b/docs/documentation/guides/organization-structure.mdx index 6fd672164..3cba64678 100644 --- a/docs/documentation/guides/organization-structure.mdx +++ b/docs/documentation/guides/organization-structure.mdx @@ -6,40 +6,55 @@ description: "Learn how to structure your projects, secrets, and other resources Infisical is designed to provide comprehensive, centralized, and efficient management of secrets, certificates, and encryption keys within organizations. Below is an overview of Infisical's structured components, which developers and administrators can leverage for optimal project management and security posture. -### 1. Projects +### 0. Cluster/Instance + +- **Best Practice**: In most cases, a single Infisical instance or cluster is sufficient. Multiple clusters are typically only necessary for large, globally distributed organizations. +- **Use Cases**: + - **Cloud-hosted** deployments typically use a single cluster. While technically possible, using multiple clusters is not a common practice and is generally unnecessary. + - **Self-hosted** deployments can be configured with multiple clusters if needed. + + +### 1. Organization + +- **Definition**: An Infisical [organization](/documentation/platform/organization) is a set of projects that use the same billing. +- **Use Cases**: + - In **self-hosted** setups, you can create multiple organizations (e.g., one for each department or business unit). + - In **cloud-hosted deployments**, it's standard to use a single organization. + +### 2. Projects - **Definition and Role**: [Projects](/documentation/platform/project) are the highest-level construct within an [organization](/documentation/platform/organization) in Infisical. They serve as the primary container for all functionalities. - **Correspondence to Code Repositories**: Projects typically align with specific code repositories. - **Functional Capabilities**: Each project encompasses features for managing secrets, certificates, and encryption keys, serving as the central hub for these resources. -### 2. Environments +### 3. Environments - **Purpose**: Environments are designed for organizing and compartmentalizing secrets within projects. - **Customization Options**: Environments can be tailored to align with existing infrastructure setups of any project. Default options include **Development**, **Staging**, and **Production**. - **Structure**: Each environment inherently has a root level for storing secrets, but additional sub-organizations can be created through [folders](/documentation/platform/folder) for better secret management. -### 3. Folders +### 4. Folders - **Use Case**: Folders are available for more advanced organizational needs, allowing logical separation of secrets. - **Typical Structure**: Folders can correspond to specific logical units, such as microservices or different layers of an application, providing refined control over secrets. -### 4. Imports +### 5. Imports - **Purpose and Benefits**: To promote reusability and avoid redundancy, Infisical supports the use of imports. This allows secrets, folders, or entire environments to be referenced across multiple projects as needed. - **Best Practice**: Utilizing [secret imports](/documentation/platform/secret-reference#secret-imports) or [references](/documentation/platform/secret-reference#secret-referencing) ensures consistency and minimizes manual overhead. -### 5. Approval Workflows +### 6. Approval Workflows - **Importance**: Implementing approval workflows is recommended for organizations aiming to enhance efficiency and strengthen their security posture. - **Types of Workflows**: - **[Access Requests](/documentation/platform/pr-workflows)**: This workflow allows developers to request access to sensitive resources. Such access can be configured for temporary use, a practice known as "just-in-time" access. - **[Change Requests](/documentation/platform/access-controls/access-requests)**: Facilitates reviews and approvals when changes are proposed for sensitive environments or specific folders, ensuring proper oversight. -### 6. Access Controls +### 7. Access Controls Infisical’s access control framework is unified for both human users and machine identities, ensuring consistent management across the board. -### 6.1 Roles +### 7.1 Roles - **2 Role Types**: - **Organization-Level Roles**: Provide broad access across the organization (e.g., ability to manage billing, configure settings, etc.). @@ -49,17 +64,17 @@ Infisical’s access control framework is unified for both human users and machi Project access is defined not via an organization-level role, but rather through specific project memberships of both human and machine identities. Admin roles bypass this by default. -### 6.2 Additional Privileges +### 7.2 Additional Privileges [Additional privileges](/documentation/platform/access-controls/additional-privileges) can be assigned to users and machines on an ad-hoc basis for specific scenarios where roles alone are insufficient. If you find yourself using additional privileges too much, it is recommended to create custom roles. Additional privileges can be temporary or permanent. -### 6.3 Attribute-Based Access Control (ABAC) +### 7.3 Attribute-Based Access Control (ABAC) [Attribute-based Access Controls](/documentation/platform/access-controls/attribute-based-access-controls) allow restrictions based on tags or attributes linked to secrets. These can be integrated with SAML assertions and other security frameworks for dynamic access management. -### 6.4 User Groups +### 7.4 User Groups - **Application**: Organizations should use users groups in situations when they have a lot of developers with the same level of access (e.g., separated by team, department, seniority, etc.). - **Synchronization**: [User groups](/documentation/platform/groups) can be synced with an identity provider to maintain consistency and reduce manual management. diff --git a/docs/documentation/platform/dynamic-secrets/overview.mdx b/docs/documentation/platform/dynamic-secrets/overview.mdx index 1f17869ef..43e8867ab 100644 --- a/docs/documentation/platform/dynamic-secrets/overview.mdx +++ b/docs/documentation/platform/dynamic-secrets/overview.mdx @@ -41,3 +41,16 @@ Dynamic secrets are particularly useful in environments with stringent security 4. [Oracle](./oracle) 6. [Redis](./redis) 5. [AWS IAM](./aws-iam) + +**FAQ** + + + + This usually happens when the SQL statements defined for creating or revoking the secret are not compatible with your database provider. + + Different SQL engines have different expectations for quoting identifiers and values. For example, some use backticks (`` `username` ``), others use single quotes (`'username'`), and some expect double quotes (`"username"`). A statement that works on one provider might fail on another. + + **Recommendation:** + Make sure to adjust your SQL statements to follow the syntax required by your specific database provider. Always test them directly on your target database to ensure they execute without errors. + + diff --git a/docs/documentation/platform/kms-configuration/aws-kms.mdx b/docs/documentation/platform/kms-configuration/aws-kms.mdx index 598a3a32b..3fc5404ae 100644 --- a/docs/documentation/platform/kms-configuration/aws-kms.mdx +++ b/docs/documentation/platform/kms-configuration/aws-kms.mdx @@ -19,7 +19,7 @@ Before you begin, you'll first need to choose a method of authentication with AW ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png) 2. Select **AWS Account** as the **Trusted Entity Type**. - 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted. + 3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted. 4. Optionally, enable **Require external ID** and enter your Infisical **project ID** to further enhance security. diff --git a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx index 3845a3879..0fd43f2c4 100644 --- a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx +++ b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx @@ -1,5 +1,5 @@ --- -title: "Auth0 Client Secret" +title: "Auth0 Client Secret Rotation" description: "Learn how to automatically rotate Auth0 Client Secrets." --- diff --git a/docs/documentation/platform/secret-rotation/aws-iam-user-secret.mdx b/docs/documentation/platform/secret-rotation/aws-iam-user-secret.mdx new file mode 100644 index 000000000..1e8eb3950 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/aws-iam-user-secret.mdx @@ -0,0 +1,191 @@ +--- +title: "AWS IAM User" +description: "Learn how to automatically rotate Access Key Id and Secret Key of AWS IAM Users." +--- + +Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical +at a specified interval or on-demand. + +## Prerequisites + +- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Rotation** permissions +- Make sure to add the following permissions to your IAM Role/IAM User Permission policy set used by your AWS Connection: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:ListAccessKeys", + "iam:CreateAccessKey", + "iam:UpdateAccessKey", + "iam:DeleteAccessKey", + "iam:ListUsers" + ], + "Resource": "*" + } + ] + } + ``` + +## Workflow + +The typical workflow for using the AWS IAM User rotation strategy consists of four steps: + +1. Creating the target IAM user whose credentials you wish to rotate. +2. Configuring the rotation strategy in Infisical with the credentials of the managing IAM user. +3. Pressing the **Rotate** button in the Infisical dashboard to trigger the rotation of the target IAM user's credentials. The strategy can also be configured to rotate the credentials automatically at a specified interval. + +In the following steps, we explore the end-to-end workflow for setting up this strategy in Infisical. + + + + To begin, create an IAM user whose credentials you wish to rotate. If you already have an IAM user, + then you can skip this step. + + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **AWS IAM User Secret** option. + ![Select AWS IAM User Secret](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-option.png) + + 3. Select the **AWS Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-configuration.png) + + - **AWS Connection** - the connection that will perform the rotation of the specified application's Client Secret. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + 4. Select the AWS IAM user and the region of the user whose credentials you want to rotate. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-parameters.png) + + 5. Specify the secret names that the AWS IAM access key credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-secrets-mapping.png) + + - **Access Key ID** - the name of the secret that the AWS access key ID will be mapped to. + - **Secret Access Key** - the name of the secret that the rotated secret access key will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-confirm.png) + + 8. Your **AWS IAM User** credentials are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-created.png) + + + To create an AWS IAM User Rotation, make an API request to the [Create AWS IAM User Rotation](/api-reference/endpoints/secret-rotations/aws-iam-user-secret/create) API endpoint. + + You will first need the **User Name** of the AWS IAM user you want to rotate the secret for. This can be obtained from the IAM console, on Users tab. + ![Users](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-user-names.png) + + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/aws-iam-user-secret \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-aws-rotation", + "projectId": "9602cfc5-20b9-4c35-a056-dd7372db0f25", + "description": "My rotation strategy description", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 2, + "rotateAtUtc": { + "hours": 11.5, + "minutes": 29.5 + }, + "parameters": { + "userName": "testUser", + "region": "us-east-1" + }, + "secretsMapping": { + "accessKeyId": "AWS_ACCESS_KEY_ID", + "secretAccessKey": "AWS_SECRET_ACCESS_KEY" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-aws-rotation", + "description": "My rotation strategy description", + "secretsMapping": { + "accessKeyId": "AWS_ACCESS_KEY_ID", + "secretAccessKey": "AWS_SECRET_ACCESS_KEY" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 123, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": null, + "nextRotationAt": "2023-11-07T05:31:56Z", + "isLastRotationManual": true, + "connection": { + "app": "aws", + "name": "my-aws-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "9602cfc5-20b9-4c35-a056-dd7372db0f25", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 11.5, + "minutes": 29.5 + }, + "lastRotationMessage": null, + "type": "aws-iam-user-secret", + "parameters": { + "userName": "testUser", + "region": "us-east-1" + } + } + } + ``` + + + + + +**FAQ** + + + + There are a few reasons for why this might happen: + - The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target AWS region is incorrect, etc.) + - The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup + [paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary + permissions to rotate the credentials. + + diff --git a/docs/documentation/platform/secret-rotation/aws-iam.mdx b/docs/documentation/platform/secret-rotation/aws-iam.mdx deleted file mode 100644 index c524abfbc..000000000 --- a/docs/documentation/platform/secret-rotation/aws-iam.mdx +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: "AWS IAM User" -description: "Learn how to automatically rotate Access Key Id and Secret Key of AWS IAM Users." ---- - -Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical -at a specified interval or on-demand. - -## Workflow - -The typical workflow for using the AWS IAM User rotation strategy consists of four steps: - -1. Creating the target IAM user whose credentials you wish to rotate. -2. Creating the managing IAM user used by Infisical to rotate the credentials of the target IAM user. -3. Configuring the rotation strategy in Infisical with the credentials of the managing IAM user. -4. Pressing the **Rotate** button in the Infisical dashboard to trigger the rotation of the target IAM user's credentials. The strategy can also be configured to rotate the credentials automatically at a specified interval. - -In the following steps, we explore the end-to-end workflow for setting up this strategy in Infisical. - - - - To begin, create an IAM user whose credentials you wish to rotate. If you already have an IAM user, - then you can skip this step. - - - Next, create another IAM user to be used by Infisical to rotate the credentials of the IAM user in the previous step. - - 2.1. In your AWS console, head to IAM > Access management > Users and press **Create user**. - - ![iam user secret rotation create user](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-user.png) - - 2.2. Next, give the user a username like **infisical-rotation-manager** and press **Next**. - - ![iam user secret rotation username](../../../images/platform/secret-rotation/aws-iam/rotation-manager-username.png) - - 2.3. Next, in the **Set permissions** step, select **Attach policies directly** and then press **Create policy**. - - ![iam user secret rotation create policy](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-policy.png) - - 2.4. Next, in the **Policy editor**, paste the following JSON and press **Next**: - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": [ - "iam:DeleteAccessKey", - "iam:GetAccessKeyLastUsed", - "iam:CreateAccessKey" - ], - "Resource": "*" - } - ] - } - ``` - - - The IAM policy above uses the wildcard option in Resource: "*". - - You may want to restrict the policy to a specific path, and make any adjustments as necessary, to control access for the managing user in production. - - Read more about this [here](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/). - - - In the **Review and create** step, give the policy a name like **infisical-rotation-manager**, press **Create policy** to finish creating the policy. - - ![iam user secret rotation policy review](../../../images/platform/secret-rotation/aws-iam/rotation-manager-policy-review.png) - - 2.5. Back in the **Set permissions** step from step 2.3, refresh the policy list and search for the policy you just created from step 2.4. - - Select the policy and press **Next**. - - ![iam user secret rotation attach policy](../../../images/platform/secret-rotation/aws-iam/rotation-manager-attach-policy.png) - - In the **Review and create** step, press **Create user** to finish creating the IAM user. - - ![iam user secret rotation manager user review](../../../images/platform/secret-rotation/aws-iam/rotation-manager-user-review.png) - - 2.5. Having created the user, head to its Security credentials > Access keys and press **Create access key**. - - Follow the subsequent steps to create the **access key** and **secret access key** credential pair for the user. - - ![iam user secret rotation manager create access key](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-access-key.png) - - At the end of the flow, copy the **Access key** and **Secret access key** to use when configuring the AWS IAM User rotation strategy back in Infisical next. - - ![iam user secret rotation manager access keys](../../../images/platform/secret-rotation/aws-iam/rotation-manager-access-keys.png) - - - 3.1. Back in Infisical, head to the Project > Secrets > Environment and path where you want the rotated AWS IAM credentials to appear and create two placeholder secrets. - - In this example, we'll create two secrets called `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`. - - ![iam user secret rotation secrets](../../../images/platform/secret-rotation/aws-iam/rotation-config-secrets.png) - - 3.2. Next, in the **Secret Rotation** tab, press on the **AWS IAM** tile to configure the AWS IAM User rotation strategy. - - ![iam user secret rotation select aws iam user method](../../../images/platform/secret-rotation/aws-iam/rotations-select-aws-iam-user.png) - - 3.3. Input the configuration details for the AWS IAM User rotation strategy obtained from steps 1 and 2: - - ![iam user secret rotation config 1](../../../images/platform/secret-rotation/aws-iam/rotation-config-1.png) - - Here's some guidance on each field: - - - Manager User Access Key: The managing IAM user's access key from step 2.5. - - Manager User Secret Key: The managing IAM user's secret access key from step 2.5. - - Manager User AWS Region: The [AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) for Infisical to make requests to such as `us-east-1`. - - IAM Username: The IAM username of the user from step 1. - - Next, specify the output secret mappings configuration for the rotated AWS IAM credentials; this is the secrets whose values will be replaced with new credentials after each rotation. - Here, you can also specify a rotation interval for the credentials to be automatically rotated periodically. - - In this example, we want to map the output of the rotated AWS IAM credentials to the secrets that we created in step 3.1 (i.e. `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`). - - ![iam user secret rotation config 2](../../../images/platform/secret-rotation/aws-iam/rotation-config-2.png) - - Finally, press **Submit** to create the secret rotation strategy. - - - You should now see the AWS IAM User rotation strategy listed in the **Secret Rotation** tab. - - To manually trigger a rotation, you can press the **Rotate** button on the strategy. - Once triggered, the secrets in step 3.1 should be updated with new rotated credential values. - - ![iam user secret rotations aws iam user](../../../images/platform/secret-rotation/aws-iam/rotations-aws-iam-user.png) - - - -**FAQ** - - - - There are a few reasons for why this might happen: - - - The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target IAM username is incorrect, etc.). - - The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup [paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary permissions to rotate the credentials. - - The target IAM user already has 2 access keys configured in AWS; you should delete one of the access keys to allow for rotation. - - \ No newline at end of file diff --git a/docs/documentation/platform/secret-rotation/ldap-password.mdx b/docs/documentation/platform/secret-rotation/ldap-password.mdx new file mode 100644 index 000000000..103fe4656 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/ldap-password.mdx @@ -0,0 +1,173 @@ +--- +title: "LDAP Password Rotation" +description: "Learn how to automatically rotate LDAP passwords." +--- + + + Due to how LDAP passwords are rotated, retired credentials will not be able to + authenticate with the LDAP provider during their [inactive period](./overview#how-rotation-works). + + This is a limitation of the LDAP provider and cannot be + rectified by Infisical. + + +## Prerequisites + +- Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements + +## Create an LDAP Password Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **LDAP Password** option. + ![Select LDAP Password](/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png) + + 3. Select the **LDAP Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png) + + - **LDAP Connection** - the connection that will perform the rotation of the configured DN's password. + + LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol. + + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + Due to LDAP Password Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + + 4. Specify the Distinguished Name (DN) of the principal whose password you want to rotate and configure the password requirements. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png) + + 5. Specify the secret names that the client credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png) + + - **DN** - the name of the secret that the principal's Distinguished Name (DN) will be mapped to. + - **Password** - the name of the secret that the rotated password will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/ldap-password/ldap-password-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png) + + 8. Your **LDAP Password** credentials are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/ldap-password/ldap-password-created.png) + + + To create an LDAP Password Rotation, make an API request to the [Create LDAP + Password Rotation](/api-reference/endpoints/secret-rotations/ldap-password/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/ldap-password \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-ldap-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my ldap password rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": false, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "dn": "CN=John,CN=Users,DC=example,DC=com", + "passwordRequirements": { + "length": 48, + "required": { + "digits": 2, + "lowercase": 2, + "uppercase": 2, + "symbols": 2 + }, + "allowedSymbols": "-_.~!*" + } + }, + "secretsMapping": { + "dn": "LDAP_DN", + "password": "LDAP_PASSWORD" + } + }' + ``` + + + Due to LDAP Password Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-ldap-rotation", + "description": "my ldap password rotation", + "secretsMapping": { + "dn": "LDAP_DN", + "password": "LDAP_PASSWORD" + }, + "isAutoRotationEnabled": false, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "ldap", + "name": "my-ldap-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "ldap-password", + "parameters": { + "dn": "CN=John,CN=Users,DC=example,DC=com", + "passwordRequirements": { + "length": 48, + "required": { + "digits": 2, + "lowercase": 2, + "uppercase": 2, + "symbols": 2 + }, + "allowedSymbols": "-_.~!*" + } + } + } + } + ``` + + diff --git a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx index 8789e4152..c20622f26 100644 --- a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx @@ -1,5 +1,5 @@ --- -title: "Microsoft SQL Server Credentials" +title: "Microsoft SQL Server Credentials Rotation" description: "Learn how to automatically rotate Microsoft SQL Server credentials." --- diff --git a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx index e0606e6ab..55175d967 100644 --- a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx @@ -1,5 +1,5 @@ --- -title: "PostgreSQL Credentials" +title: "PostgreSQL Credentials Rotation" description: "Learn how to automatically rotate PostgreSQL credentials." --- diff --git a/docs/documentation/platform/ssh.mdx b/docs/documentation/platform/ssh.mdx index ae1e43df5..2bc433f75 100644 --- a/docs/documentation/platform/ssh.mdx +++ b/docs/documentation/platform/ssh.mdx @@ -10,10 +10,10 @@ Infisical SSH can be configured to provide users on your team short-lived, secur and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management, unauthorized access, and SSH key sprawl. -The following entities and concepts are important to understand when using Infisical SSH: +The following entities are important to understand when configuring and using Infisical SSH: - Administrator: An individual on your team who is responsible for configuring Infisical SSH. -- Users: Other individuals on your team that need access to the remote host. +- Users: Other individuals that gain access to remote hosts through Infisical SSH. - Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH. ## Workflow @@ -72,7 +72,7 @@ we will register a remote host with Infisical through a [machine identity](/docu Next, use the `infisical ssh add-host` command to register the remote host with Infisical. As part of this command, input the ID of the Infisical SSH project you created in step 1 for the `--projectId` flag and the hostname of the remote host for the `--hostname` flag. ```bash - sudo infisical ssh add-host --projectId= --hostname= --token="$INFISICAL_TOKEN" --writeUserCaToFile --writeHostCertToFile --configureSshd + sudo infisical ssh add-host --projectId= --hostname= --token="$INFISICAL_TOKEN" --write-user-ca-to-file --write-host-cert-to-file --configure-sshd ``` @@ -136,44 +136,66 @@ Once Infisical SSH is configured by an administrator, users can SSH to the remot Follow the instructions [here](/cli/overview) to install the Infisical CLI onto your local machine. - - Run the `infisical login` command to authenticate with Infisical. - - ```bash - infisical login - ``` - - Run the `infisical ssh connect` command to connect to a remote host. + The `infisical ssh connect` command can be used in either interactive or non-interactive mode to connect to a remote host. - ```bash - infisical ssh connect - ``` + + + In interactive mode, you'll first need to authenticate with Infisical by running: - You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by - the administrator. + ```bash + infisical login + ``` - ```bash - Use the arrow keys to navigate: ↓ ↑ → ← - ? Select an SSH Host: - ▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com - ``` + Then simply run: - After selecting a host, you'll be prompted to select a login user from a list of allowed login users: + ```bash + infisical ssh connect + ``` - ```bash - ? Select Login User: - ▸ ec2-user - ``` + You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by + the administrator. - If successful, you should be able to SSH to the remote host. + ```bash + Use the arrow keys to navigate: ↓ ↑ → ← + ? Select an SSH Host: + ▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com + ``` - ```bash - ✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com - ✔ ec2-user - ✔ SSH credentials successfully added to agent - Connecting to ec2-user@ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com... - ``` + After selecting a host, you'll be prompted to select a login user from a list of allowed login users: + + ```bash + ? Select Login User: + ▸ ec2-user + ``` + + If successful, you should be able to SSH to the remote host. + + ```bash + ✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com + ✔ ec2-user + ✔ SSH credentials successfully added to agent + Connecting to ec2-user@ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com... + ``` + + + For CI/CD pipelines or automation scenarios, you can use the non-interactive mode with an Infisical token: + + ```bash + infisical ssh connect \ + --hostname ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com \ + --login-user ec2-user \ + --out-file-path ~/.ssh/id_rsa-cert.pub \ + --token + ``` + + This will: + - Connect to the specified hostname + - Use the specified login user + - Write the SSH credentials to the specified path instead of adding them to the SSH agent + - Authenticate using the provided Infisical token + + diff --git a/docs/images/app-connections/aws/iam-role-secret-rotation-permissions.png b/docs/images/app-connections/aws/iam-role-secret-rotation-permissions.png new file mode 100644 index 000000000..6d99922f8 Binary files /dev/null and b/docs/images/app-connections/aws/iam-role-secret-rotation-permissions.png differ diff --git a/docs/images/app-connections/general/add-connection.png b/docs/images/app-connections/general/add-connection.png index 97718065a..ad9d54716 100644 Binary files a/docs/images/app-connections/general/add-connection.png and b/docs/images/app-connections/general/add-connection.png differ diff --git a/docs/images/app-connections/ldap/create-simple-bind-method.png b/docs/images/app-connections/ldap/create-simple-bind-method.png new file mode 100644 index 000000000..e7fff6789 Binary files /dev/null and b/docs/images/app-connections/ldap/create-simple-bind-method.png differ diff --git a/docs/images/app-connections/ldap/select-ldap-connection.png b/docs/images/app-connections/ldap/select-ldap-connection.png new file mode 100644 index 000000000..48465df67 Binary files /dev/null and b/docs/images/app-connections/ldap/select-ldap-connection.png differ diff --git a/docs/images/app-connections/ldap/simple-bind-connection.png b/docs/images/app-connections/ldap/simple-bind-connection.png new file mode 100644 index 000000000..8013a4687 Binary files /dev/null and b/docs/images/app-connections/ldap/simple-bind-connection.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-app-connection-created.png b/docs/images/app-connections/teamcity/teamcity-app-connection-created.png new file mode 100644 index 000000000..698894139 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-app-connection-created.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-app-connection-modal.png b/docs/images/app-connections/teamcity/teamcity-app-connection-modal.png new file mode 100644 index 000000000..9e602dc1b Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-app-connection-modal.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-app-connection-option.png b/docs/images/app-connections/teamcity/teamcity-app-connection-option.png new file mode 100644 index 000000000..52d001126 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-app-connection-option.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-main-page.png b/docs/images/app-connections/teamcity/teamcity-main-page.png new file mode 100644 index 000000000..4ee08d774 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-main-page.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-copy.png b/docs/images/app-connections/teamcity/teamcity-token-copy.png new file mode 100644 index 000000000..1aa363104 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-copy.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-created.png b/docs/images/app-connections/teamcity/teamcity-token-created.png new file mode 100644 index 000000000..c909f4107 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-created.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-page.png b/docs/images/app-connections/teamcity/teamcity-token-page.png new file mode 100644 index 000000000..1730a99ef Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-page.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-popup.png b/docs/images/app-connections/teamcity/teamcity-token-popup.png new file mode 100644 index 000000000..0e18d37ea Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-popup.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-configuration.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-configuration.png new file mode 100644 index 000000000..0e530600b Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-configuration.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-confirm.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-confirm.png new file mode 100644 index 000000000..545e8625a Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-confirm.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-created.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-created.png new file mode 100644 index 000000000..51f28107e Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-created.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-details.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-details.png new file mode 100644 index 000000000..272c93958 Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-details.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-option.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-option.png new file mode 100644 index 000000000..92fcc22cd Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-option.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-parameters.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-parameters.png new file mode 100644 index 000000000..1ccfa4d6c Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-parameters.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-secrets-mapping.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-secrets-mapping.png new file mode 100644 index 000000000..83d7157dd Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-user-names.png b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-user-names.png new file mode 100644 index 000000000..b8fa47ae3 Binary files /dev/null and b/docs/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-user-names.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png new file mode 100644 index 000000000..91a2f2fb3 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png new file mode 100644 index 000000000..1725c4355 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png new file mode 100644 index 000000000..4172ec7f7 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png new file mode 100644 index 000000000..ed41c13ad Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png new file mode 100644 index 000000000..dfe723b06 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png new file mode 100644 index 000000000..997073bc5 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png b/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png new file mode 100644 index 000000000..4dd500fe1 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png differ diff --git a/docs/images/secret-syncs/general/secret-sync-tab.png b/docs/images/secret-syncs/general/secret-sync-tab.png index dad8c2426..5317fabf0 100644 Binary files a/docs/images/secret-syncs/general/secret-sync-tab.png and b/docs/images/secret-syncs/general/secret-sync-tab.png differ diff --git a/docs/images/secret-syncs/teamcity/select-teamcity-option.png b/docs/images/secret-syncs/teamcity/select-teamcity-option.png new file mode 100644 index 000000000..261f53163 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/select-teamcity-option.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-created.png b/docs/images/secret-syncs/teamcity/teamcity-sync-created.png new file mode 100644 index 000000000..871b94db9 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-created.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-destination.png b/docs/images/secret-syncs/teamcity/teamcity-sync-destination.png new file mode 100644 index 000000000..bc546fc3b Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-destination.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-details.png b/docs/images/secret-syncs/teamcity/teamcity-sync-details.png new file mode 100644 index 000000000..02b890926 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-details.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-options.png b/docs/images/secret-syncs/teamcity/teamcity-sync-options.png new file mode 100644 index 000000000..d9ef23fb0 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-options.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-review.png b/docs/images/secret-syncs/teamcity/teamcity-sync-review.png new file mode 100644 index 000000000..753b7ba17 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-review.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-source.png b/docs/images/secret-syncs/teamcity/teamcity-sync-source.png new file mode 100644 index 000000000..dc69e1db8 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-source.png differ diff --git a/docs/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png b/docs/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png new file mode 100644 index 000000000..b18fd7713 Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png differ diff --git a/docs/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png new file mode 100644 index 000000000..ee5f9257b Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png differ diff --git a/docs/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png new file mode 100644 index 000000000..9a8ae13cb Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png differ diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index ab2f4638f..195f98247 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -55,7 +55,7 @@ Infisical supports two methods for connecting to AWS. ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png) 2. Select **AWS Account** as the **Trusted Entity Type**. - 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. + 3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. 4. (Recommended) Enable "Require external ID" and input your **Organization ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). @@ -146,6 +146,34 @@ Infisical supports two methods for connecting to AWS. + + + + Use the following custom policy to grant the minimum permissions required by Infisical to rotate secrets to AWS Access Keys: + + ![IAM Role Secret Rotation Permissions](/images/app-connections/aws/iam-role-secret-rotation-permissions.png) + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:ListAccessKeys", + "iam:CreateAccessKey", + "iam:UpdateAccessKey", + "iam:DeleteAccessKey", + "iam:ListUsers" + ], + "Resource": "*" + } + ] + } + ``` + + + @@ -293,6 +321,34 @@ Infisical supports two methods for connecting to AWS. + + + + Use the following custom policy to grant the minimum permissions required by Infisical to rotate secrets to AWS Access Keys: + + ![IAM Role Secret Rotation Permissions](/images/app-connections/aws/iam-role-secret-rotation-permissions.png) + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:ListAccessKeys", + "iam:CreateAccessKey", + "iam:UpdateAccessKey", + "iam:DeleteAccessKey", + "iam:ListUsers" + ], + "Resource": "*" + } + ] + } + ``` + + + @@ -362,4 +418,5 @@ Infisical supports two methods for connecting to AWS. + diff --git a/docs/integrations/app-connections/ldap.mdx b/docs/integrations/app-connections/ldap.mdx new file mode 100644 index 000000000..63c4bfed1 --- /dev/null +++ b/docs/integrations/app-connections/ldap.mdx @@ -0,0 +1,96 @@ +--- +title: "LDAP Connection" +description: "Learn how to configure an LDAP Connection for Infisical." +--- + +Infisical supports the use of [Simple Binding](https://ldap.com/the-ldap-bind-operation) to connect with your LDAP provider. + +## Prerequisites + +You will need the following information to establish an LDAP connection: + +- **LDAP URL** - The LDAP/LDAPS URL to connect to (e.g., ldap://domain-or-ip:389 or ldaps://domain-or-ip:636) +- **Binding DN** - The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com') +- **Binding Password** - The password to bind with for authentication +- **CA Certificate** - The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate + +Depending on how you intend to use your LDAP connection, there may be additional requirements: + + + + + For Password Rotation, the following requirements must additionally be met: + - You must use an LDAPS connection + - The binding user must either have: + - Permission to change other users passwords if rotating directory users' passwords + - Permission to update their own password if rotating their personal password + + + + + +## Setup LDAP Connection in Infisical + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **LDAP Connection** option. + ![Select LDAP Connection](/images/app-connections/ldap/select-ldap-connection.png) + + 3. Select the **Simple Bind** method option and provide the details obtained from the previous section and press **Connect to Provider**. + ![Create LDAP Connection](/images/app-connections/ldap/create-simple-bind-method.png) + + 4. Your **LDAP Connection** is now available for use. + ![Assume Role LDAP Connection](/images/app-connections/ldap/simple-bind-connection.png) + + + To create an LDAP Connection, make an API request to the [Create LDAP + Connection](/api-reference/endpoints/app-connections/ldap/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/ldap \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-ldap-connection", + "method": "simple-bind", + "credentials": { + "provider": "active-directory", + "url": "ldaps://domain-or-ip:636", + "dn": "CN=John,CN=Users,DC=example,DC=com", + "password": "", + "sslRejectUnauthorized": true, + "sslCertificate": "..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-ldap-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "ldap", + "method": "simple-bind", + "credentials": { + "provider": "active-directory", + "url": "ldaps://domain-or-ip:636", + "dn": "CN=John,CN=Users,DC=example,DC=com", + "sslRejectUnauthorized": true, + "sslCertificate": "..." + } + } + } + ``` + + diff --git a/docs/integrations/app-connections/teamcity.mdx b/docs/integrations/app-connections/teamcity.mdx new file mode 100644 index 000000000..1ffafe637 --- /dev/null +++ b/docs/integrations/app-connections/teamcity.mdx @@ -0,0 +1,119 @@ +--- +title: "TeamCity Connection" +description: "Learn how to configure a TeamCity Connection for Infisical." +--- + +Infisical supports connecting to TeamCity using an Access Token to securely sync your secrets to TeamCity. + +## Setup TeamCity Connection in Infisical + + + + Navigate to the TeamCity **Profile** page by clicking on your profile icon in the bottom-left corner. + ![TeamCity Main Page](/images/app-connections/teamcity/teamcity-main-page.png) + + + Select the **Access Tokens** tab from the left sidebar navigation menu. + ![TeamCity Token Page](/images/app-connections/teamcity/teamcity-token-page.png) + + + Click the **Create access token** button and provide a name for your token (e.g., "Infisical Integration"). You may set an expiration date or leave it blank for no expiry. + The permission scope can either be **Same as current user** or **Limit per project**. + + If you're choosing **Limit per project**, make sure you select the relevant project and enable the permissions relevant to your use case: + + + + - View build configuration settings + - Edit project + + + + ![TeamCity Token Popup](/images/app-connections/teamcity/teamcity-token-popup.png) + + + Setting your permission scope to **Same as current user** will allow your integration to access multiple projects as long as the current user has read and write access to them. + + + If you configure an expiry date for your access token, you must manually rotate to a new token before the expiration date to prevent service interruption. + + + + After creation, a modal with the Access Token will be displayed. Copy this token immediately and store it securely, as you won't be able to view it again after closing this dialog. + ![TeamCity Token Copy Popup](/images/app-connections/teamcity/teamcity-token-copy.png) + + + You should now see your newly created token in the list of access tokens. + ![TeamCity Token Created](/images/app-connections/teamcity/teamcity-token-created.png) + + + + + 1. Navigate to App Connections + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + 2. Add Connection + + Click the **+ Add Connection** button and select the **TeamCity Connection** option from the available integrations. + ![Select TeamCity Connection](/images/app-connections/teamcity/teamcity-app-connection-option.png) + 3. Fill the TeamCity Connection Modal + + Complete the TeamCity Connection form by entering: + - A descriptive name for the connection + - The Access Token you generated in steps 3-4 + - The URL of your TeamCity instance + - An optional description for future reference + + ![TeamCity Connection Modal](/images/app-connections/teamcity/teamcity-app-connection-modal.png) + 4. Connection Created + + After clicking Create, your **TeamCity Connection** is established and ready to use with your Infisical projects. + ![TeamCity Connection Created](/images/app-connections/teamcity/teamcity-app-connection-created.png) + + + To create a TeamCity Connection, make an API request to the [Create TeamCity + Connection](/api-reference/endpoints/app-connections/teamcity/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/teamcity \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-teamcity-connection", + "method": "access-token", + "credentials": { + "accessToken": "...", + "instanceUrl": "https://yourcompany.teamcity.com" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-teamcity-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "teamcity", + "method": "access-token", + "credentials": { + "instanceUrl": "https://yourcompany.teamcity.com" + } + } + } + ``` + + + + diff --git a/docs/integrations/cloud/teamcity.mdx b/docs/integrations/cloud/teamcity.mdx index 3e713cc6a..0b58f1b80 100644 --- a/docs/integrations/cloud/teamcity.mdx +++ b/docs/integrations/cloud/teamcity.mdx @@ -3,43 +3,6 @@ title: "TeamCity" description: "How to sync secrets from Infisical to TeamCity" --- -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a TeamCity Access Token in Profile > Access Tokens - - ![integrations teamcity dashboard](../../images/integrations/teamcity/integrations-teamcity-dashboard.png) - ![integrations teamcity token](../../images/integrations/teamcity/integrations-teamcity-token.png) - - - For this integration to work, the TeamCity Access Token must either have the - **Same as current user** account-wide permission enabled or, if **Limit per project** - is selected, then it must at minimum have the **View build configuration settings** and **Edit project** permissions enabled. - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the TeamCity tile and input your TeamCity Access Token and Server URL to grant Infisical access to your TeamCity account. - - ![integrations teamcity authorization](../../images/integrations/teamcity/integrations-teamcity-auth.png) - - - - Select which Infisical environment secrets you want to sync to which TeamCity project (and optionally build configuration) and press create integration to start syncing secrets to TeamCity. - - ![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity-create.png) - - - Infisical integrates with both TeamCity's project-level and build configuration-level environment variables. - - To sync secrets to a specific build configuration in a TeamCity project, you can select a build configuration from the **TeamCity Build Config** dropdown; otherwise, leaving it empty will sync secrets to TeamCity at the project-level. - - - ![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity.png) - - + + The TeamCity Native Integration will be deprecated in 2026. Please migrate to our new [TeamCity Sync](../secret-syncs/teamcity). + diff --git a/docs/integrations/cloud/windmill.mdx b/docs/integrations/cloud/windmill.mdx index d0b2b9643..7d4c2cc82 100644 --- a/docs/integrations/cloud/windmill.mdx +++ b/docs/integrations/cloud/windmill.mdx @@ -3,39 +3,6 @@ title: "Windmill" description: "How to sync secrets from Infisical to Windmill" --- -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a [Windmill](https://www.windmill.dev/) access token in Access Tokens - - ![integrations windmill dashboard](../../images/integrations/windmill/integrations-windmill-dashboard.png) - ![integrations windmill token](../../images/integrations/windmill/integrations-windmill-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Windmill tile and input your Windmill access token to grant Infisical access to your Windmill account. - - ![integrations windmill authorization](../../images/integrations/windmill/integrations-windmill-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Windmill workspace and press create integration to start syncing secrets to Windmill. - - ![integrations windmill](../../images/integrations/windmill/integrations-windmill-create.png) - ![integrations windmill](../../images/integrations/windmill/integrations-windmill.png) - - - Secrets synced to Windmill are subject to the [ownership path - prefix](https://www.windmill.dev/docs/core_concepts/roles_and_permissions) - convention of Windmill. Accordingly, all secrets must be prefixed with either - `u/` or `f/` for user-based and folder-based secret along with the name of the - secret. Put differently, you must use the full path of the secret as its name - in Infisical to be considered valid such as `u/user/FOO/BAR`. - - - \ No newline at end of file + + The Windmill Native Integration will be deprecated in 2026. Please migrate to our new [Windmill Sync](../secret-syncs/windmill). + diff --git a/docs/integrations/secret-syncs/teamcity.mdx b/docs/integrations/secret-syncs/teamcity.mdx new file mode 100644 index 000000000..e79fc0f0c --- /dev/null +++ b/docs/integrations/secret-syncs/teamcity.mdx @@ -0,0 +1,147 @@ +--- +title: "TeamCity Sync" +description: "Learn how to configure a TeamCity Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [TeamCity Connection](/integrations/app-connections/teamcity) with the required **Secret Sync** permissions + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **TeamCity** option. + ![Select TeamCity](/images/secret-syncs/teamcity/select-teamcity-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/teamcity/teamcity-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/teamcity/teamcity-sync-destination.png) + + - **TeamCity Connection**: The TeamCity Connection to authenticate with. + - **Project**: The TeamCity project to sync secrets to. + - **Build Configuration**: The build configuration to sync secrets to. + + + Not including a Build Configuration will sync secrets to the entire project. + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/teamcity/teamcity-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over TeamCity when keys conflict. + - **Import Secrets (Prioritize TeamCity)**: Imports secrets from the destination endpoint before syncing, prioritizing values from TeamCity over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your TeamCity Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/teamcity/teamcity-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your TeamCity Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/teamcity/teamcity-sync-review.png) + + 8. If enabled, your TeamCity Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/teamcity/teamcity-sync-created.png) + + + + To create a **TeamCity Sync**, make an API request to the [Create TeamCity Sync](/api-reference/endpoints/secret-syncs/teamcity/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/teamcity \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-teamcity-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "project": "TestProject", + "buildConfig": "TestBuildConfig" + } + }' + ``` + + + The **Project** and **Build Config** parameters must use project and build configuration IDs, not their names. + + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-teamcity-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "teamcity", + "name": "my-teamcity-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "teamcity", + "destinationConfig": { + "project": "TestProject", + "buildConfig": "TestBuildConfig" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index ee07fb048..47a4c0a76 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -179,8 +179,10 @@ "pages": [ "documentation/platform/secret-rotation/overview", "documentation/platform/secret-rotation/auth0-client-secret", - "documentation/platform/secret-rotation/postgres-credentials", - "documentation/platform/secret-rotation/mssql-credentials" + "documentation/platform/secret-rotation/aws-iam-user-secret", + "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mssql-credentials", + "documentation/platform/secret-rotation/postgres-credentials" ] }, { @@ -313,6 +315,13 @@ "self-hosting/deployment-options/kubernetes-helm" ] }, + { + "group": "Linux Package", + "pages": [ + "self-hosting/deployment-options/native/linux-package/installation", + "self-hosting/deployment-options/native/linux-package/commands-configuration" + ] + }, "self-hosting/guides/upgrading-infisical", "self-hosting/configuration/envars", "self-hosting/configuration/requirements", @@ -329,7 +338,8 @@ "pages": [ "self-hosting/reference-architectures/aws-ecs", "self-hosting/reference-architectures/linux-deployment-ha", - "self-hosting/reference-architectures/on-prem-k8s-ha" + "self-hosting/reference-architectures/on-prem-k8s-ha", + "self-hosting/reference-architectures/google-cloud-run" ] }, "self-hosting/ee", @@ -424,8 +434,10 @@ "integrations/app-connections/gcp", "integrations/app-connections/github", "integrations/app-connections/humanitec", + "integrations/app-connections/ldap", "integrations/app-connections/mssql", "integrations/app-connections/postgres", + "integrations/app-connections/teamcity", "integrations/app-connections/terraform-cloud", "integrations/app-connections/vercel", "integrations/app-connections/windmill" @@ -449,6 +461,7 @@ "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel", "integrations/secret-syncs/windmill" @@ -558,9 +571,7 @@ }, { "group": "Others", - "pages": [ - "integrations/external/backstage" - ] + "pages": ["integrations/external/backstage"] }, { "group": "", @@ -861,6 +872,32 @@ "api-reference/endpoints/secret-rotations/auth0-client-secret/update" ] }, + { + "group": "AWS IAM User Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/create", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/list", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/update" + ] + }, + { + "group": "LDAP Password", + "pages": [ + "api-reference/endpoints/secret-rotations/ldap-password/create", + "api-reference/endpoints/secret-rotations/ldap-password/delete", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-name", + "api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/list", + "api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets", + "api-reference/endpoints/secret-rotations/ldap-password/update" + ] + }, { "group": "Microsoft SQL Server Credentials", "pages": [ @@ -1013,6 +1050,18 @@ "api-reference/endpoints/app-connections/humanitec/delete" ] }, + { + "group": "LDAP", + "pages": [ + "api-reference/endpoints/app-connections/ldap/list", + "api-reference/endpoints/app-connections/ldap/available", + "api-reference/endpoints/app-connections/ldap/get-by-id", + "api-reference/endpoints/app-connections/ldap/get-by-name", + "api-reference/endpoints/app-connections/ldap/create", + "api-reference/endpoints/app-connections/ldap/update", + "api-reference/endpoints/app-connections/ldap/delete" + ] + }, { "group": "Microsoft SQL Server", "pages": [ @@ -1037,6 +1086,18 @@ "api-reference/endpoints/app-connections/postgres/delete" ] }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/app-connections/teamcity/list", + "api-reference/endpoints/app-connections/teamcity/available", + "api-reference/endpoints/app-connections/teamcity/get-by-id", + "api-reference/endpoints/app-connections/teamcity/get-by-name", + "api-reference/endpoints/app-connections/teamcity/create", + "api-reference/endpoints/app-connections/teamcity/update", + "api-reference/endpoints/app-connections/teamcity/delete" + ] + }, { "group": "Terraform Cloud", "pages": [ @@ -1202,6 +1263,20 @@ "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" ] }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/secret-syncs/teamcity/list", + "api-reference/endpoints/secret-syncs/teamcity/get-by-id", + "api-reference/endpoints/secret-syncs/teamcity/get-by-name", + "api-reference/endpoints/secret-syncs/teamcity/create", + "api-reference/endpoints/secret-syncs/teamcity/update", + "api-reference/endpoints/secret-syncs/teamcity/delete", + "api-reference/endpoints/secret-syncs/teamcity/sync-secrets", + "api-reference/endpoints/secret-syncs/teamcity/import-secrets", + "api-reference/endpoints/secret-syncs/teamcity/remove-secrets" + ] + }, { "group": "Terraform Cloud", "pages": [ diff --git a/docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx b/docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx new file mode 100644 index 000000000..61be021b6 --- /dev/null +++ b/docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx @@ -0,0 +1,38 @@ +--- +title: "Configurations" +description: "Learn how to configure and manage the Infisical Linux package" +--- + +## Configuration Overview + +All configuration for the Infisical Linux package is managed through a single file called `infisical.rb`, located in the `/etc/infisical` directory. +This file defines all necessary settings, including encryption keys, database connections, and environment-specific settings. + + After making any changes to the `infisical.rb` file, always run `infisical-ctl reconfigure` to apply them. + +### Example Configuration + +```ruby infisical.rb +# Important: Replace these values with secure keys in production +infisical_core['ENCRYPTION_KEY'] = '6c1fe4e407b8911c104518103505b218' +infisical_core['AUTH_SECRET'] = '5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=' + +# Database connection strings +infisical_core['DB_CONNECTION_URI'] = 'postgres://:@:5432/' +infisical_core['REDIS_URL'] = 'redis://:6379' +``` + +For a full list of supported configuration variables, refer to the [configuration variables documentation](/self-hosting/configuration/envars). + +## All `infisical-ctl` Commands + +The Infisical Linux package includes the `infisical-ctl` command-line tool, which allows you to manage your deployment. +The available commands are listed below. + +| Command | Description | +|-----------------------------|-----------------------------------------------------------------------------| +| `infisical-ctl reconfigure` | Applies changes from `infisical.rb` and restarts the Infisical services. | +| `infisical-ctl start` | Starts the Infisical services. | +| `infisical-ctl stop` | Stops all running Infisical services. | +| `infisical-ctl status` | Displays the current status of the Infisical services. | +| `infisical-ctl tail` | Streams real-time logs from the Infisical application. | \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/native/linux-package/installation.mdx b/docs/self-hosting/deployment-options/native/linux-package/installation.mdx new file mode 100644 index 000000000..30ef83242 --- /dev/null +++ b/docs/self-hosting/deployment-options/native/linux-package/installation.mdx @@ -0,0 +1,122 @@ +--- +title: "Installation" +description: "Learn how to deploy Infisical using the Linux package" +--- + +Infisical can be deployed on Linux virtual machines without the need for containers using our standalone Linux packages. +These packages are available in both .deb (for Debian-based systems) and .rpm (for RHEL-based systems) formats. +The installation includes the Infisical service, along with a CLI tool (infisical-ctl) to help you manage configurations, startup, and application logging. +This approach is ideal for environments where containerization isn't desired, while still providing a lightweight deployment option. + +## Prerequisites + +This installation method only provides the Infisical application. You are responsible for configuring both PostgreSQL and Redis, either by using managed services (e.g., AWS RDS, Azure Database, GCP Cloud SQL/Memorystore) or by deploying them manually in your on-prem environment. +Please ensure you have the following before beginning installation of Infisical: + +- A Linux server running a Debian/Ubuntu or RHEL-based distribution +- A running PostgreSQL database instance (version 14 and up) +- A running Redis database instance (versions 6.x or 7.x) + +## Installation Steps + + + + + Select your Linux distribution to get started. Only AMD64-based systems are supported at this time, ARM support is coming soon. + + + + + Add the Infisical repository: + ```bash + curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.deb.sh' | sudo -E bash + ``` + + Install Infisical: + ```bash + sudo apt-get update && sudo apt-get install -y infisical-core + ``` + + > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). + + + + Add the Infisical repository: + ```bash + curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.rpm.sh' | sudo -E bash + ``` + + Install Infisical: + ```bash + sudo yum install infisical-core + ``` + + > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). + + + + + Verify the installation: + ```bash + infisical-ctl help + ``` + + + + Create an `infisical.rb` file at `/etc/infisical`. This file contains your database connection strings and other runtime settings. + + ```ruby + # Important: Replace with secure values in production + infisical_core['ENCRYPTION_KEY'] = '6c1fe4e407b8911c104518103505b218' + infisical_core['AUTH_SECRET'] = '5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=' + + # Example database connection strings + infisical_core['DB_CONNECTION_URI'] = 'postgres://:@:/' + infisical_core['REDIS_URL'] = 'redis://:' + ``` + + See the full list of options in our [configuration documentation](/self-hosting/configuration/envars). + + + + 1. Start the Infisical service: + ```bash + infisical-ctl reconfigure + ``` + The server runs on port `8080` by default (customizable in `infisical.rb`). + + 2. Check the service status: + ```bash + infisical-ctl status + ``` + + View the service logs in real-time: + ```bash + infisical-ctl tail + ``` + + + + +## Platform Support + +### Microsoft Windows +Infisical is built for Linux-based systems. It is not supported on Microsoft Windows, and we do not plan to support it in the near future. For Windows users, consider running Infisical in a virtual machine or WSL2 environment. + +### Unsupported Linux Distributions and Unix-like Systems +Infisical is not tested or officially supported on the following: + +- Arch Linux +- Fedora +- FreeBSD +- Gentoo +- macOS + +We recommend sticking to officially supported distributions for the best experience. + +## Linux vs Containerized Deployments + +Infisical is a stateless application, which means it can be easily scaled and redeployed without maintaining internal state between instances. + +If your use case requires rolling updates, self-healing, or auto-scaling, we recommend deploying Infisical in a containerized environment such as Kubernetes/OpenShift, or using managed container orchestration services like AWS ECS or Google Cloud Run. +These platforms offer built-in capabilities for high availability and help simplify operational overhead for your deployment. \ No newline at end of file diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index a7ea50e39..acb692711 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -33,21 +33,10 @@ Choose from a number of deployment options listed below to get started. Use our Helm chart to Install Infisical on your Kubernetes cluster. -{/* - - Install Infisical on your Debian-based system without containers using our standalone binary. - - - Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box. - - */} + Install Infisical on your system without containers using our Linux package. + diff --git a/docs/self-hosting/reference-architectures/google-cloud-run.mdx b/docs/self-hosting/reference-architectures/google-cloud-run.mdx new file mode 100644 index 000000000..36fa1de24 --- /dev/null +++ b/docs/self-hosting/reference-architectures/google-cloud-run.mdx @@ -0,0 +1,114 @@ +--- +title: "Google Cloud Run" +description: "Reference architecture for self-hosting Infisical on Google Cloud Run." +--- + +## Overview +This guide outlines a reference architecture for deploying Infisical in a self-hosted configuration using Google Cloud Run. +It is intended to provide a scalable, secure, and production-ready baseline for organizations choosing Google Cloud Platform (GCP) as their infrastructure provider. + +## Core Components + +- **Cloud Run:** Infisical service is containerized and deployed as fully managed Cloud Run services. + +- **Cloud SQL:** Infisical uses Postgres as its persistence layer. As such, Cloud SQL for PostgreSQL is used. + +- **MemoryStore for Redis:** To schedule jobs, process audit logs and cache performance, Infisical requires Redis. + +## Securing Infisical's root credential + +- **Secrets Manager:** To secure Infisical’s root credentials (database connection string, encryption key, etc.), +we highly recommend that you use Google Secrets Manager and only allow the tasks running Infisical to access them. + +## High Availability and Scalability + +This architecture leverages Google Cloud's managed services to achieve high availability and scalability out of the box: + +**Cloud Run:** + +- Automatically scales the number of container instances up or down based on incoming request volume. +- Supports rapid scaling during traffic spikes, ensuring low latency. +- Configurable minimum and maximum instances to handle baseline and peak loads. + +**Cloud SQL:** + +- Provides high availability configurations (regional instances with automatic failover) to ensure database uptime. +- Automated backups, point-in-time recovery, and maintenance. + +**MemoryStore:** + +- Offers highly available Redis configurations with replication. +- Fully managed with automatic scaling and patching. + +**Cloud Load Balancer:** + +- Distributes user traffic across available Cloud Run instances. +- Provides SSL termination, global load balancing, and health checks. + + + **Note:** To further improve performance and availability, consider enabling multi-region deployment strategies, + regional VPC Connectors, and database replicas for read-heavy workloads. + + +## Configuration + + + + **Cloud SQL (PostgreSQL):** + - Create a Cloud SQL instance. + - Under `Zonal availability`, select the `Multiple zones` option to ensure High Availability. + - Configure private IP access. + + **MemoryStore (Redis):** + - Deploy a Redis instance. + - Configure VPC access. + + + + Visit [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) and select a version of Infisical image you would like to deploy. + Then, within Cloud Run, paste the URL of the specific Infisical Docker image you would like to use within the `Container image URL` field. + + ![Cloud Run container image settings UI](/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png) + + Remember to replace `` with the docker image tag of your choice. + + + For a minimal installation of Infisical, you must configure the following environment variables: + + ```bash + ENCRYPTION_KEY= + AUTH_SECRET= + DB_CONNECTION_URI="" + SITE_URL="" + REDIS_URL="" + ``` + [View all available configurations](/self-hosting/configuration/envars). + + You will want to setup Postgres and Redis within Google Cloud Platform to connect to Infisical. + + Once you have added the required environment variables to the `Environment Variables` section within Cloud Run, + create the container to get Infisical up and running. + + ![Cloud Run container environment variables settings UI](/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png) + + + The above environment variable values are only to be used as an example and should not be used in production + + + + + + Enable `Connect to a VPC for outbound traffic`: This enables the service to talk to private resources (e.g., a Cloud SQL database, Redis instance on a private IP) inside your Google Cloud VPC network. + + Select `Send traffic directly to a VPC`: It gives lower latency and better performance, but uses more IPs from the subnet. + + + Your Cloud Run revision must be in the same VPC network + + + ![Cloud Run container network settings UI](/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png) + + Once the container is running, verify the installation by opening your web browser and navigating to the Site URL. + + + \ No newline at end of file diff --git a/frontend/public/images/integrations/LDAP.png b/frontend/public/images/integrations/LDAP.png new file mode 100644 index 000000000..4cf290176 Binary files /dev/null and b/frontend/public/images/integrations/LDAP.png differ diff --git a/frontend/public/lotties/notification-bell.json b/frontend/public/lotties/notification-bell.json new file mode 100644 index 000000000..56d4c9950 --- /dev/null +++ b/frontend/public/lotties/notification-bell.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":90,"w":500,"h":500,"nm":"system-regular-46-notification-bell","ddd":0,"assets":[{"id":"comp_1","nm":"hover-bell","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.231],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.313],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":7.041,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19,"s":[-53]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":29,"s":[-20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[-62]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":49,"s":[-20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":58,"s":[-62]},{"i":{"x":[0.283],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":69.713,"s":[14]},{"t":79,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.231,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[249.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.313,"y":1},"o":{"x":0.333,"y":0},"t":7.041,"s":[269.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":19,"s":[111.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.313,"y":1},"o":{"x":0.333,"y":0},"t":29,"s":[121.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[111.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":49,"s":[121.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":58,"s":[111.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.283,"y":1},"o":{"x":0.333,"y":0},"t":69.713,"s":[261.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"t":79,"s":[249.998,67.334,0]}],"ix":2,"l":2},"a":{"a":0,"k":[249.998,67.334,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,6.468],[0,0],[60.406,0],[0,0],[0,-60.406],[0,0],[2.893,-5.786],[0,0],[0,0]],"o":[[-2.893,-5.786],[0,0],[0,-60.406],[0,0],[-60.406,0],[0,0],[0,6.468],[0,0],[0,0],[0,0]],"v":[[113.773,55.671],[109.375,37.038],[109.375,-20.834],[0.001,-130.21],[-0.001,-130.21],[-109.375,-20.834],[-109.375,37.038],[-113.773,55.671],[-151.042,130.21],[151.042,130.21]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.998,229.166],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.832],[0,20.832]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.998,78.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[27]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[-26]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[-28]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[-28]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[29]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":83,"s":[-6]},{"t":90,"s":[0]}],"ix":10},"p":{"a":0,"k":[249.998,182.041,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-219,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-25.889,0],[0,25.889],[0,0]],"o":[[0,0],[0,25.889],[25.888,0],[0,0],[0,0]],"v":[[-46.751,-119.666],[-46.875,-5.21],[0.001,41.666],[46.875,-5.21],[46.998,-119.666]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[23]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":57,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":67,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[21]},{"t":90,"s":[22.538]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[68]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[68]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":57,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[68]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":67,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[75]},{"t":90,"s":[77.077]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0.43],[0,0],[-2.48,0],[0,-2.48],[0,0],[-0.19,-0.38],[0,0]],"o":[[0,0],[0.19,-0.38],[0,0],[0,-2.48],[2.48,0],[0,0],[0,0.42],[0,0],[0,0]],"v":[[-6.042,4.5],[-4.792,2.01],[-4.502,0.78],[-4.502,-2],[-0.002,-6.5],[4.498,-2],[4.498,0.78],[4.788,2.01],[6.038,4.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0.19],[0,0],[2.96,0.37],[0,0],[0.41,0],[0,-0.41],[0,0],[0,-3.05],[0,0],[0.08,-0.17],[0,0],[-0.13,-0.22],[-0.26,0],[0,0],[-0.14,0.23],[0.12,0.23]],"o":[[-0.08,-0.18],[0,0],[0,-3.05],[0,0],[0,-0.41],[-0.41,0],[0,0],[-2.96,0.37],[0,0],[0,0.19],[0,0],[-0.12,0.23],[0.14,0.22],[0,0],[0.26,0],[0.14,-0.22],[0,0]],"v":[[6.128,1.34],[5.998,0.78],[5.998,-2],[0.748,-7.95],[0.748,-9.25],[-0.002,-10],[-0.752,-9.25],[-0.752,-7.95],[-6.002,-2],[-6.002,0.78],[-6.132,1.34],[-7.922,4.92],[-7.892,5.65],[-7.252,6],[7.248,6],[7.888,5.64],[7.918,4.91]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.83,0],[0,0.83],[0,0],[0,0]],"o":[[0,0.83],[-0.83,0],[0,0],[0,0],[0,0]],"v":[[1.498,7],[-0.002,8.5],[-1.502,7],[-1.501,5.544],[1.499,5.544]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-1.65,0],[0,1.65],[0,0]],"o":[[0,0],[0,1.65],[1.65,0],[0,0],[0,0]],"v":[[-3.001,5.088],[-3.002,7],[-0.002,10],[2.998,7],[2.999,5.088]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0.43],[0,0],[-2.48,0],[0,-2.48],[0,0],[-0.19,-0.38],[0,0]],"o":[[0,0],[0.19,-0.38],[0,0],[0,-2.48],[2.48,0],[0,0],[0,0.42],[0,0],[0,0]],"v":[[-6.042,4.5],[-4.792,2.01],[-4.502,0.78],[-4.502,-2],[-0.002,-6.5],[4.498,-2],[4.498,0.78],[4.788,2.01],[6.038,4.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0.19],[0,0],[2.96,0.37],[0,0],[0.41,0],[0,-0.41],[0,0],[0,-3.05],[0,0],[0.08,-0.17],[0,0],[-0.13,-0.22],[-0.26,0],[0,0],[-0.14,0.23],[0.12,0.23]],"o":[[-0.08,-0.18],[0,0],[0,-3.05],[0,0],[0,-0.41],[-0.41,0],[0,0],[-2.96,0.37],[0,0],[0,0.19],[0,0],[-0.12,0.23],[0.14,0.22],[0,0],[0.26,0],[0.14,-0.22],[0,0]],"v":[[6.128,1.34],[5.998,0.78],[5.998,-2],[0.748,-7.95],[0.748,-9.25],[-0.002,-10],[-0.752,-9.25],[-0.752,-7.95],[-6.002,-2],[-6.002,0.78],[-6.132,1.34],[-7.922,4.92],[-7.892,5.65],[-7.252,6],[7.248,6],[7.888,5.64],[7.918,4.91]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":300,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.83,0],[0,0.83],[0,0],[0,0]],"o":[[0,0.83],[-0.83,0],[0,0],[0,0],[0,0]],"v":[[1.498,7],[-0.002,8.5],[-1.502,7],[-1.501,5.544],[1.499,5.544]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-1.65,0],[0,1.65],[0,0]],"o":[[0,0],[0,1.65],[1.65,0],[0,0],[0,0]],"v":[[-3.001,5.088],[-3.002,7],[-0.002,10],[2.998,7],[2.999,5.088]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":300,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":291,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-bell","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":100,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-bell","dr":90}],"props":{}} \ No newline at end of file diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx index d2340f42d..420889d2b 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx @@ -1,8 +1,6 @@ -import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay"; -import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; import { TAuth0ClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation"; -import { ViewRotationGeneratedCredentialsDisplay } from "./shared"; +import { CredentialDisplay, ViewRotationGeneratedCredentialsDisplay } from "./shared"; type Props = { generatedCredentialsResponse: TAuth0ClientSecretRotationGeneratedCredentialsResponse; @@ -17,40 +15,23 @@ export const ViewAuth0ClientSecretRotationGeneratedCredentials = ({ const inactiveCredentials = generatedCredentials[inactiveIndex]; return ( - <> - - {activeCredentials?.clientId} - - {activeCredentials?.clientSecret} - - - } - inactiveCredentials={ - <> - {inactiveCredentials?.clientId} - - {inactiveCredentials?.clientSecret} - - - } - /> - -

- Due to how Auth0 client secrets are rotated, retired credentials will not be able to - authenticate with Auth0 during their{" "} - - inactive period - - . This is a limitation of the Auth0 platform and cannot be rectified by Infisical. -

-
- + + {activeCredentials?.clientId} + + {activeCredentials?.clientSecret} + + + } + inactiveCredentials={ + <> + {inactiveCredentials?.clientId} + + {inactiveCredentials?.clientSecret} + + + } + /> ); }; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAwsIamUserSecretRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAwsIamUserSecretRotationGeneratedCredentials.tsx new file mode 100644 index 000000000..b07a615ff --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAwsIamUserSecretRotationGeneratedCredentials.tsx @@ -0,0 +1,42 @@ +import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay"; +import { TAwsIamUserSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation"; + +import { ViewRotationGeneratedCredentialsDisplay } from "./shared"; + +type Props = { + generatedCredentialsResponse: TAwsIamUserSecretRotationGeneratedCredentialsResponse; +}; + +export const ViewAwsIamUserSecretRotationGeneratedCredentials = ({ + generatedCredentialsResponse: { generatedCredentials, activeIndex } +}: Props) => { + const inactiveIndex = activeIndex === 0 ? 1 : 0; + + const activeCredentials = generatedCredentials[activeIndex]; + const inactiveCredentials = generatedCredentials[inactiveIndex]; + + return ( + + + {activeCredentials?.accessKeyId} + + + {activeCredentials?.secretAccessKey} + + + } + inactiveCredentials={ + <> + + {inactiveCredentials?.accessKeyId} + + + {inactiveCredentials?.secretAccessKey} + + + } + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx new file mode 100644 index 000000000..238dabea3 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx @@ -0,0 +1,41 @@ +import { TLdapPasswordRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; + +import { CredentialDisplay, ViewRotationGeneratedCredentialsDisplay } from "./shared"; + +type Props = { + generatedCredentialsResponse: TLdapPasswordRotationGeneratedCredentialsResponse; +}; + +export const ViewLdapPasswordRotationGeneratedCredentials = ({ + generatedCredentialsResponse: { generatedCredentials, activeIndex } +}: Props) => { + const inactiveIndex = activeIndex === 0 ? 1 : 0; + + const activeCredentials = generatedCredentials[activeIndex]; + const inactiveCredentials = generatedCredentials[inactiveIndex]; + + return ( + + + {activeCredentials?.dn} + + + {activeCredentials?.password} + + + } + inactiveCredentials={ + <> + + {inactiveCredentials?.dn} + + + {inactiveCredentials?.password} + + + } + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx index 7d594b0f5..d5af51eef 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx @@ -4,8 +4,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { ViewAuth0ClientSecretRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials"; +import { ViewLdapPasswordRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials"; import { Modal, ModalContent, Spinner } from "@app/components/v2"; -import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; +import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; +import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; +import { + IS_ROTATION_DUAL_CREDENTIALS, + SECRET_ROTATION_CONNECTION_MAP, + SECRET_ROTATION_MAP +} from "@app/helpers/secretRotationsV2"; import { SecretRotation, TSecretRotationV2, @@ -13,6 +20,7 @@ import { } from "@app/hooks/api/secretRotationsV2"; import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared"; +import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials"; type Props = { secretRotation?: TSecretRotationV2; @@ -67,13 +75,46 @@ const Content = ({ secretRotation }: ContentProps) => { /> ); break; + case SecretRotation.LdapPassword: + Component = ( + + ); + break; + case SecretRotation.AwsIamUserSecret: + Component = ( + + ); + break; default: throw new Error("Unhandled View Generated Credential Rotation Type"); } + const appName = APP_CONNECTION_MAP[SECRET_ROTATION_CONNECTION_MAP[type]].name; + return (
{Component} + {!IS_ROTATION_DUAL_CREDENTIALS[type] && ( + +

+ Due to {SECRET_ROTATION_MAP[type].name} Rotations utilizing a single credential set, + retired credentials will not be able to authenticate with {appName} during their{" "} + + inactive period + + . This is a limitation of {appName} and cannot be rectified by Infisical. +

+
+ )} {nextRotationAt && (
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/AwsIamUserSecretRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/AwsIamUserSecretRotationParametersFields.tsx new file mode 100644 index 000000000..525bde198 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/AwsIamUserSecretRotationParametersFields.tsx @@ -0,0 +1,84 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { AwsRegionSelect } from "@app/components/secret-syncs/forms/SecretSyncDestinationFields/shared"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { TAwsIamUserSecret, useListAwsConnectionIamUsers } from "@app/hooks/api/appConnections/aws"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const AwsIamUserSecretRotationParametersFields = () => { + const { control, watch } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.AwsIamUserSecret; + } + >(); + + const connectionId = watch("connection.id"); + + const { data: clients, isPending: isClientsPending } = useListAwsConnectionIamUsers({ + connectionId + }); + + return ( + <> + ( + Ensure that your connection has the correct permissions.} + > +
+ Don't see the IAM user you're looking for?{" "} + +
+ + } + > + client.UserName === value) ?? ""} + onChange={(option) => { + onChange((option as SingleValue)?.UserName ?? ""); + }} + options={clients} + placeholder="Select an IAM user..." + getOptionLabel={(option) => + (option as SingleValue)?.UserName ?? "" + } + getOptionValue={(option) => + (option as SingleValue)?.UserName ?? "" + } + /> +
+ )} + /> + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx new file mode 100644 index 000000000..9c9d8329f --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx @@ -0,0 +1,169 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { DEFAULT_PASSWORD_REQUIREMENTS } from "@app/components/secret-rotations-v2/forms/schemas/shared"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const LdapPasswordRotationParametersFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.LdapPassword; + } + >(); + + return ( + <> + ( + + + + )} + /> +
+
+ Password Requirements +
+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(e.target.value)} + /> + + )} + /> +
+
+ + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx index 444510e1e..cdbf63111 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx @@ -4,12 +4,16 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2Form } from "../schemas"; import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields"; +import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields"; +import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields"; import { SqlCredentialsRotationParametersFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields, + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields }; export const SecretRotationV2ParametersFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/AwsIamUserSecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/AwsIamUserSecretRotationReviewFields.tsx new file mode 100644 index 000000000..d84c0753f --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/AwsIamUserSecretRotationReviewFields.tsx @@ -0,0 +1,30 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +import { SecretRotationReviewSection } from "./shared"; + +export const AwsIamUserSecretRotationReviewFields = () => { + const { watch } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.AwsIamUserSecret; + } + >(); + + const [parameters, { accessKeyId, secretAccessKey }] = watch(["parameters", "secretsMapping"]); + + return ( + <> + + {parameters.region} + {parameters.userName} + + + {accessKeyId} + {secretAccessKey} + + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx new file mode 100644 index 000000000..1ffcad139 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx @@ -0,0 +1,29 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +import { SecretRotationReviewSection } from "./shared"; + +export const LdapPasswordRotationReviewFields = () => { + const { watch } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.LdapPassword; + } + >(); + + const [parameters, { dn, password }] = watch(["parameters", "secretsMapping"]); + + return ( + <> + + {parameters.dn} + + + {dn} + {password} + + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index 4fb3b6d24..23ee25d7f 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -7,12 +7,16 @@ import { getRotateAtLocal } from "@app/helpers/secretRotationsV2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields"; +import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields"; +import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields"; import { SqlCredentialsRotationReviewFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields, + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields }; export const SecretRotationV2ReviewFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/AwsIamUserSecretRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/AwsIamUserSecretRotationSecretsMappingFields.tsx new file mode 100644 index 000000000..2c6432122 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/AwsIamUserSecretRotationSecretsMappingFields.tsx @@ -0,0 +1,58 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2"; + +import { SecretsMappingTable } from "./shared"; + +export const AwsIamUserSecretRotationSecretsMappingFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.AwsIamUserSecret; + } + >(); + + const { rotationOption } = useSecretRotationV2Option(SecretRotation.AwsIamUserSecret); + + const items = [ + { + name: "Access Key ID", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.accessKeyId" + /> + ) + }, + { + name: "Secret Access Key", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.secretAccessKey" + /> + ) + } + ]; + + return ; +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx new file mode 100644 index 000000000..01d2e0d74 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx @@ -0,0 +1,58 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2"; + +import { SecretsMappingTable } from "./shared"; + +export const LdapPasswordRotationSecretsMappingFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.LdapPassword; + } + >(); + + const { rotationOption } = useSecretRotationV2Option(SecretRotation.LdapPassword); + + const items = [ + { + name: "DN", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.dn" + /> + ) + }, + { + name: "Password", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.password" + /> + ) + } + ]; + + return ; +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index 58277d593..16bffe6cf 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -4,12 +4,16 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2Form } from "../schemas"; import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields"; +import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields"; +import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields"; import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields, + [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields }; export const SecretRotationV2SecretsMappingFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema.ts new file mode 100644 index 000000000..a8ead3bed --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const AwsIamUserSecretRotationSchema = z + .object({ + type: z.literal(SecretRotation.AwsIamUserSecret), + parameters: z.object({ + userName: z.string().trim().min(1, "User Name required"), + region: z.string().trim().optional() + }), + secretsMapping: z.object({ + accessKeyId: z.string().trim().min(1, "Access Key ID required"), + secretAccessKey: z.string().trim().min(1, "Secret Access Key required") + }) + }) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index 295e199fe..6dd65b0bb 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -1,13 +1,17 @@ import { z } from "zod"; import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/auth0-client-secret-rotation-schema"; +import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema"; +import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema"; import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema"; import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema"; const SecretRotationUnionSchema = z.discriminatedUnion("type", [ PostgresCredentialsRotationSchema, MsSqlCredentialsRotationSchema, - Auth0ClientSecretRotationSchema + Auth0ClientSecretRotationSchema, + LdapPasswordRotationSchema, + AwsIamUserSecretRotationSchema ]); export const SecretRotationV2FormSchema = SecretRotationUnionSchema; diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts new file mode 100644 index 000000000..e18609f04 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { PasswordRequirementsSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared"; +import { DistinguishedNameRegex } from "@app/helpers/string"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const LdapPasswordRotationSchema = z + .object({ + type: z.literal(SecretRotation.LdapPassword), + parameters: z.object({ + dn: z + .string() + .trim() + .regex(DistinguishedNameRegex, "Invalid Distinguished Name format") + .min(1, "Distinguished Name (DN) required"), + passwordRequirements: PasswordRequirementsSchema.optional() + }), + secretsMapping: z.object({ + dn: z.string().trim().min(1, "Distinguished Name (DN) required"), + password: z.string().trim().min(1, "Password required") + }) + }) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts index 44b4c194f..284b705e4 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts @@ -1 +1,2 @@ +export * from "./password-requirements-schema"; export * from "./sql-credentials-rotation-schema"; diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts new file mode 100644 index 000000000..a02852ec8 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export const PasswordRequirementsSchema = z + .object({ + length: z + .number() + .min(1, "Password length must be a positive number") + .max(250, "Password length must be less than 250"), + required: z.object({ + digits: z.number().min(0, "Digit count must be non-negative"), + lowercase: z.number().min(0, "Lowercase count must be non-negative"), + uppercase: z.number().min(0, "Uppercase count must be non-negative"), + symbols: z.number().min(0, "Symbol count must be non-negative") + }), + allowedSymbols: z + .string() + .regex(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/, "Invalid symbols") + .optional() + .transform((value) => value || "-_.~!*") + }) + .refine( + (data) => { + return Object.values(data.required).some((count) => count > 0); + }, + { + message: "At least one character type must be required", + path: ["required.digits"] + } + ) + .refine( + (data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, + { message: "Sum of required characters cannot exceed the total length", path: ["length"] } + ); + +export const DEFAULT_PASSWORD_REQUIREMENTS = { + length: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + }, + allowedSymbols: "-_.~!*" +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index b2008b4f6..47afc6f04 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -12,6 +12,7 @@ import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; +import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { VercelSyncFields } from "./VercelSyncFields"; import { WindmillSyncFields } from "./WindmillSyncFields"; @@ -46,6 +47,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Windmill: return ; + case SecretSync.TeamCity: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx new file mode 100644 index 000000000..4f2718089 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx @@ -0,0 +1,128 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { + TTeamCityProjectWithBuildTypes, + useTeamCityConnectionListProjects +} from "@app/hooks/api/appConnections/teamcity"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const TeamCitySyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.TeamCity } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: projects, isLoading: isProjectsLoading } = useTeamCityConnectionListProjects( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + // For Build Config dropdown + const selectedProjectId = useWatch({ name: "destinationConfig.project", control }); + const selectedProject = projects?.find((proj) => proj.id === selectedProjectId); + + const buildTypes = selectedProject?.buildTypes?.buildType || []; + + return ( + <> + { + setValue("destinationConfig.project", ""); + setValue("destinationConfig.buildConfig", ""); + }} + /> + + ( + +
+ Don't see the project you're looking for?{" "} + +
+ + } + > + proj.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("destinationConfig.buildConfig", ""); + }} + options={projects} + placeholder="Select a project..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + ( + +
+ Don't see the configuration you're looking for?{" "} + +
+ + } + > + buildType.id === value) ?? null} + onChange={(option) => { + const selectedOption = option as SingleValue<{ id: string; name: string }>; + onChange(selectedOption?.id ?? ""); + }} + options={buildTypes} + isClearable + placeholder="Select a build configuration..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + + Not selecting a Build Configuration will sync your secrets to the entire project. + + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 79c437d27..e2c285b6f 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Camunda: case SecretSync.Vercel: case SecretSync.Windmill: + case SecretSync.TeamCity: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 99182f207..da9651535 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -22,6 +22,7 @@ import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; +import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields"; @@ -88,6 +89,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Windmill: DestinationFieldsComponent = ; break; + case SecretSync.TeamCity: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TeamCitySyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TeamCitySyncReviewFields.tsx new file mode 100644 index 000000000..277ebe1b4 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TeamCitySyncReviewFields.tsx @@ -0,0 +1,18 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const TeamCitySyncReviewFields = () => { + const { watch } = useFormContext(); + const project = watch("destinationConfig.project"); + const buildConfig = watch("destinationConfig.buildConfig"); + + return ( + <> + {project} + {buildConfig} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index d58e60b19..50221dc39 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -9,6 +9,7 @@ import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-s import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; +import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema"; @@ -25,7 +26,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ TerraformCloudSyncDestinationSchema, CamundaSyncDestinationSchema, VercelSyncDestinationSchema, - WindmillSyncDestinationSchema + WindmillSyncDestinationSchema, + TeamCitySyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/secret-syncs/forms/schemas/teamcity-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/teamcity-sync-destination-schema.ts new file mode 100644 index 000000000..e2cd60050 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/teamcity-sync-destination-schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const TeamCitySyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.TeamCity), + destinationConfig: z.object({ + project: z.string().trim().min(1, "Project required"), + buildConfig: z.string().trim().optional() + }) + }) +); diff --git a/frontend/src/components/utilities/checks/password/checkPassword.ts b/frontend/src/components/utilities/checks/password/checkPassword.ts index 6e8acaaba..e641421c6 100644 --- a/frontend/src/components/utilities/checks/password/checkPassword.ts +++ b/frontend/src/components/utilities/checks/password/checkPassword.ts @@ -31,7 +31,7 @@ interface CheckPasswordParams { * - Contains at least 1 number (0-9) or special character (emojis included) * - Does not contain 3 repeat, consecutive characters * - Does not contain any escape characters/sequences - * - Does not contain PII and/or low entropy data (eg. email address, URL, phone number, DoB, SSN, driver's license, passport) + * - Does not contain PII and/or low entropy data (eg. email address, URL, SSN) * - Is not in a database of breached passwords * * The function returns whether or not the password [password] diff --git a/frontend/src/components/utilities/checks/password/passwordRegexes.ts b/frontend/src/components/utilities/checks/password/passwordRegexes.ts index 7a70aa8f8..1e2367b49 100644 --- a/frontend/src/components/utilities/checks/password/passwordRegexes.ts +++ b/frontend/src/components/utilities/checks/password/passwordRegexes.ts @@ -20,18 +20,6 @@ export const lowEntropyRegexes = [ // URL (incl. subdomains, paths, top-level domains & query params) /^(?:(?:https?|ftp):\/\/)?(?:\w+\.)?[a-zA-Z0-9.-]+\.(?:com|org|net|edu)(?:\/\S*)?(?:\?\S*)?$/, - // Date in various formats - /(\b\d{1,4}[-/.]?\d{1,2}[-/.]?\d{1,4}\b)|(\b\d{1,4}[-/.]?\w{3}[-/.]?\d{1,4}\b)/, - - // Phone numbers (generalized) - /(?:\+(?:[1-9]\d{0,2})\s?)?(?:\(\d{1,4}\)\s?)?(?:\d[-.\s]?){5,}\d/, - - // Passport numbers (generalized) - /\b(?:[A-Z0-9]{6,9}|[A-Z0-9]{8,9}|[A-Z0-9]{9}|[A-Z0-9]{10,11})\b/, - - // Driver's license numbers (generalized) - /\b(?:[A-Z0-9]{7,10}|[A-Z0-9]{10,11}|[A-Z0-9]{7,10})\b/, - // US social security number /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/ ]; diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 2be3f3dbc..cd3c6675f 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -281,6 +281,14 @@ export const ROUTE_PATHS = Object.freeze({ "/cert-manager/$projectId/overview", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview" ), + CertificateAuthoritiesPage: setRoute( + "/cert-manager/$projectId/certificate-authorities", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities" + ), + AlertingPage: setRoute( + "/cert-manager/$projectId/alerting", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting" + ), PkiCollectionDetailsByIDPage: setRoute( "/cert-manager/$projectId/pki-collections/$collectionId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index c94bd6648..02c0fbbcb 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -1,5 +1,12 @@ import { faGithub } from "@fortawesome/free-brands-svg-icons"; -import { faKey, faLock, faPassport, faServer, faUser } from "@fortawesome/free-solid-svg-icons"; +import { + faKey, + faLink, + faLock, + faPassport, + faServer, + faUser +} from "@fortawesome/free-solid-svg-icons"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { @@ -12,9 +19,11 @@ import { GcpConnectionMethod, GitHubConnectionMethod, HumanitecConnectionMethod, + LdapConnectionMethod, MsSqlConnectionMethod, PostgresConnectionMethod, TAppConnection, + TeamCityConnectionMethod, TerraformCloudConnectionMethod, VercelConnectionMethod, WindmillConnectionMethod @@ -43,7 +52,9 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" }, [AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }, [AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" }, - [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 } + [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 }, + [AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 }, + [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -71,10 +82,13 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: return { name: "Username & Password", icon: faLock }; + case TeamCityConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: return { name: "Client Credentials", icon: faServer }; + case LdapConnectionMethod.SimpleBind: + return { name: "Simple Bind", icon: faLink }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index 1a57d37cd..8acfaafe5 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -19,20 +19,34 @@ export const SECRET_ROTATION_MAP: Record< name: "Auth0 Client Secret", image: "Auth0.png", size: 35 + }, + [SecretRotation.LdapPassword]: { + name: "LDAP Password", + image: "LDAP.png", + size: 65 + }, + [SecretRotation.AwsIamUserSecret]: { + name: "AWS IAM User Secret", + image: "Amazon Web Services.png", + size: 50 } }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, - [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, + [SecretRotation.LdapPassword]: AppConnection.LDAP, + [SecretRotation.AwsIamUserSecret]: AppConnection.AWS }; // if a rotation can potentially have downtime due to rotating a single credential set this to false export const IS_ROTATION_DUAL_CREDENTIALS: Record = { [SecretRotation.PostgresCredentials]: true, [SecretRotation.MsSqlCredentials]: true, - [SecretRotation.Auth0ClientSecret]: false + [SecretRotation.Auth0ClientSecret]: false, + [SecretRotation.LdapPassword]: false, + [SecretRotation.AwsIamUserSecret]: true }; export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 291b6f80a..009c2804b 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -39,6 +39,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, - [SecretSync.Windmill]: AppConnection.Windmill + [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.TeamCity]: AppConnection.TeamCity }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/helpers/string.ts b/frontend/src/helpers/string.ts index 109b51d49..ddd9fb7c9 100644 --- a/frontend/src/helpers/string.ts +++ b/frontend/src/helpers/string.ts @@ -12,3 +12,6 @@ export const isValidPath = (val: string): boolean => { const validPathRegex = /^[a-zA-Z0-9-_.:]+(?:\/[a-zA-Z0-9-_.:]+)*$/; return validPathRegex.test(val); }; + +export const DistinguishedNameRegex = + /^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/; diff --git a/frontend/src/hooks/api/appConnections/aws/queries.tsx b/frontend/src/hooks/api/appConnections/aws/queries.tsx index 87965507b..895ed35ee 100644 --- a/frontend/src/hooks/api/appConnections/aws/queries.tsx +++ b/frontend/src/hooks/api/appConnections/aws/queries.tsx @@ -4,15 +4,20 @@ import { apiRequest } from "@app/config/request"; import { appConnectionKeys } from "@app/hooks/api/appConnections"; import { + TAwsConnectionIamUser, TAwsConnectionKmsKey, + TAwsConnectionListIamUsersResponse, TAwsConnectionListKmsKeysResponse, + TListAwsConnectionIamUsers, TListAwsConnectionKmsKeys } from "./types"; const awsConnectionKeys = { all: [...appConnectionKeys.all, "aws"] as const, listKmsKeys: (params: TListAwsConnectionKmsKeys) => - [...awsConnectionKeys.all, "kms-keys", params] as const + [...awsConnectionKeys.all, "kms-keys", params] as const, + listIamUsers: (params: TListAwsConnectionIamUsers) => + [...awsConnectionKeys.all, "iam-users", params] as const }; export const useListAwsConnectionKmsKeys = ( @@ -40,3 +45,28 @@ export const useListAwsConnectionKmsKeys = ( ...options }); }; + +export const useListAwsConnectionIamUsers = ( + { connectionId }: TListAwsConnectionIamUsers, + options?: Omit< + UseQueryOptions< + TAwsConnectionIamUser[], + unknown, + TAwsConnectionIamUser[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: awsConnectionKeys.listIamUsers({ connectionId }), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/aws/${connectionId}/users` + ); + + return data.iamUsers; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/aws/types.ts b/frontend/src/hooks/api/appConnections/aws/types.ts index 7661b131d..d2c7c39cb 100644 --- a/frontend/src/hooks/api/appConnections/aws/types.ts +++ b/frontend/src/hooks/api/appConnections/aws/types.ts @@ -14,3 +14,18 @@ export type TAwsConnectionKmsKey = { export type TAwsConnectionListKmsKeysResponse = { kmsKeys: TAwsConnectionKmsKey[]; }; + +export type TListAwsConnectionIamUsers = { + connectionId: string; +}; + +export type TAwsConnectionIamUser = { + arn: string; + UserName: string; +}; + +export type TAwsConnectionListIamUsersResponse = { + iamUsers: TAwsConnectionIamUser[]; +}; + +export type TAwsIamUserSecret = TAwsConnectionIamUser; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 704b200bb..5b9d3fad4 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -12,5 +12,7 @@ export enum AppConnection { MsSql = "mssql", Camunda = "camunda", Windmill = "windmill", - Auth0 = "auth0" + Auth0 = "auth0", + LDAP = "ldap", + TeamCity = "teamcity" } diff --git a/frontend/src/hooks/api/appConnections/teamcity/index.ts b/frontend/src/hooks/api/appConnections/teamcity/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/teamcity/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/teamcity/queries.tsx b/frontend/src/hooks/api/appConnections/teamcity/queries.tsx new file mode 100644 index 000000000..9d117c265 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/teamcity/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TTeamCityProjectWithBuildTypes } from "./types"; + +const teamcityConnectionKeys = { + all: [...appConnectionKeys.all, "teamcity"] as const, + listProjects: (connectionId: string) => + [...teamcityConnectionKeys.all, "projects", connectionId] as const +}; + +export const useTeamCityConnectionListProjects = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TTeamCityProjectWithBuildTypes[], + unknown, + TTeamCityProjectWithBuildTypes[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: teamcityConnectionKeys.listProjects(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/teamcity/${connectionId}/projects` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/teamcity/types.ts b/frontend/src/hooks/api/appConnections/teamcity/types.ts new file mode 100644 index 000000000..a7adab034 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/teamcity/types.ts @@ -0,0 +1,13 @@ +export type TTeamCityProject = { + id: string; + name: string; +}; + +export type TTeamCityProjectWithBuildTypes = TTeamCityProject & { + buildTypes: { + buildType: { + id: string; + name: string; + }[]; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index ae57fbda2..10c1076cd 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -67,6 +67,14 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Auth0; }; +export type TLdapConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.LDAP; +}; + +export type TTeamCityConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.TeamCity; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -81,7 +89,8 @@ export type TAppConnectionOption = | TMsSqlConnectionOption | TCamundaConnectionOption | TWindmillConnectionOption - | TAuth0ConnectionOption; + | TAuth0ConnectionOption + | TTeamCityConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -98,4 +107,6 @@ export type TAppConnectionOptionMap = { [AppConnection.Camunda]: TCamundaConnectionOption; [AppConnection.Windmill]: TWindmillConnectionOption; [AppConnection.Auth0]: TAuth0ConnectionOption; + [AppConnection.LDAP]: TLdapConnectionOption; + [AppConnection.TeamCity]: TTeamCityConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 29e82bff6..2eabee1f2 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -9,8 +9,10 @@ import { TDatabricksConnection } from "./databricks-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; import { THumanitecConnection } from "./humanitec-connection"; +import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TPostgresConnection } from "./postgres-connection"; +import { TTeamCityConnection } from "./teamcity-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection"; import { TVercelConnection } from "./vercel-connection"; import { TWindmillConnection } from "./windmill-connection"; @@ -24,8 +26,10 @@ export * from "./databricks-connection"; export * from "./gcp-connection"; export * from "./github-connection"; export * from "./humanitec-connection"; +export * from "./ldap-connection"; export * from "./mssql-connection"; export * from "./postgres-connection"; +export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; @@ -44,7 +48,9 @@ export type TAppConnection = | TMsSqlConnection | TCamundaConnection | TWindmillConnection - | TAuth0Connection; + | TAuth0Connection + | TLdapConnection + | TTeamCityConnection; export type TAvailableAppConnection = Pick; @@ -86,4 +92,6 @@ export type TAppConnectionMap = { [AppConnection.Camunda]: TCamundaConnection; [AppConnection.Windmill]: TWindmillConnection; [AppConnection.Auth0]: TAuth0Connection; + [AppConnection.LDAP]: TLdapConnection; + [AppConnection.TeamCity]: TTeamCityConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/ldap-connection.ts b/frontend/src/hooks/api/appConnections/types/ldap-connection.ts new file mode 100644 index 000000000..95165fc5d --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/ldap-connection.ts @@ -0,0 +1,21 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum LdapConnectionMethod { + SimpleBind = "simple-bind" +} + +export enum LdapConnectionProvider { + ActiveDirectory = "active-directory" +} + +export type TLdapConnection = TRootAppConnection & { app: AppConnection.LDAP } & { + method: LdapConnectionMethod.SimpleBind; + credentials: { + provider: LdapConnectionProvider; + url: string; + dn: string; + sslRejectUnauthorized?: boolean; + sslCertificate?: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts b/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts new file mode 100644 index 000000000..972df6c96 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum TeamCityConnectionMethod { + AccessToken = "access-token" +} + +export type TTeamCityConnection = TRootAppConnection & { app: AppConnection.TeamCity } & { + method: TeamCityConnectionMethod.AccessToken; + credentials: { + accessToken: string; + instanceUrl: string; + }; +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 159e840ec..229f822da 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -84,6 +84,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.ADD_PKI_COLLECTION_ITEM]: "Add PKI collection item", [EventType.DELETE_PKI_COLLECTION_ITEM]: "Delete PKI collection item", [EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project", + [EventType.ORG_ADMIN_BYPASS_SSO]: "Org admin bypassed SSO enforcement", [EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template", [EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template", [EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete certificate template", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 15adb0272..d465fb820 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -90,6 +90,7 @@ export enum EventType { ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item", DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item", ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project", + ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index a18974f2e..2524f84f4 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -718,6 +718,11 @@ interface OrgAdminAccessProjectEvent { }; // no metadata yet } +interface OrgAdminBypassSSOEvent { + type: EventType.ORG_ADMIN_BYPASS_SSO; + metadata: Record; // no metadata yet +} + interface CreateCertificateTemplate { type: EventType.CREATE_CERTIFICATE_TEMPLATE; metadata: { @@ -885,6 +890,7 @@ export type Event = | AddPkiCollectionItem | DeletePkiCollectionItem | OrgAdminAccessProjectEvent + | OrgAdminBypassSSOEvent | CreateCertificateTemplate | UpdateCertificateTemplate | GetCertificateTemplate diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index d43cacb3a..4ddf4ee0c 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -1,7 +1,9 @@ export enum SecretRotation { PostgresCredentials = "postgres-credentials", MsSqlCredentials = "mssql-credentials", - Auth0ClientSecret = "auth0-client-secret" + Auth0ClientSecret = "auth0-client-secret", + LdapPassword = "ldap-password", + AwsIamUserSecret = "aws-iam-user-secret" } export enum SecretRotationStatus { diff --git a/frontend/src/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation.ts new file mode 100644 index 000000000..23397506c --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation.ts @@ -0,0 +1,38 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TAwsIamUserSecretRotation = TSecretRotationV2Base & { + type: SecretRotation.AwsIamUserSecret; + parameters: { + region?: string; + userName: string; + }; + secretsMapping: { + accessKeyId: string; + secretAccessKey: string; + }; +}; + +export type TAwsIamUserSecretRotationGeneratedCredentials = { + accessKeyId: string; + secretAccessKey: string; +}; + +export type TAwsIamUserSecretRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.AwsIamUserSecret, + TAwsIamUserSecretRotationGeneratedCredentials + >; + +export type TAwsIamUserSecretRotationOption = { + name: string; + type: SecretRotation.AwsIamUserSecret; + connection: AppConnection.AWS; + template: { + secretsMapping: TAwsIamUserSecretRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index 96d568d74..4119efc15 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -4,6 +4,16 @@ import { TAuth0ClientSecretRotationGeneratedCredentialsResponse, TAuth0ClientSecretRotationOption } from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation"; +import { + TAwsIamUserSecretRotation, + TAwsIamUserSecretRotationGeneratedCredentialsResponse, + TAwsIamUserSecretRotationOption +} from "@app/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation"; +import { + TLdapPasswordRotation, + TLdapPasswordRotationGeneratedCredentialsResponse, + TLdapPasswordRotationOption +} from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; import { TMsSqlCredentialsRotation, TMsSqlCredentialsRotationGeneratedCredentialsResponse @@ -20,13 +30,17 @@ export type TSecretRotationV2 = ( | TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation + | TLdapPasswordRotation + | TAwsIamUserSecretRotation ) & { secrets: (SecretV3RawSanitized | null)[]; }; export type TSecretRotationV2Option = | TSqlCredentialsRotationOption - | TAuth0ClientSecretRotationOption; + | TAuth0ClientSecretRotationOption + | TLdapPasswordRotationOption + | TAwsIamUserSecretRotationOption; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; @@ -35,7 +49,9 @@ export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 }; export type TViewSecretRotationGeneratedCredentialsResponse = | TPostgresCredentialsRotationGeneratedCredentialsResponse | TMsSqlCredentialsRotationGeneratedCredentialsResponse - | TAuth0ClientSecretRotationGeneratedCredentialsResponse; + | TAuth0ClientSecretRotationGeneratedCredentialsResponse + | TLdapPasswordRotationGeneratedCredentialsResponse + | TAwsIamUserSecretRotationGeneratedCredentialsResponse; export type TCreateSecretRotationV2DTO = DiscriminativePick< TSecretRotationV2, @@ -82,10 +98,14 @@ export type TSecretRotationOptionMap = { [SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption; [SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption; [SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption; + [SecretRotation.LdapPassword]: TLdapPasswordRotationOption; + [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption; }; export type TSecretRotationGeneratedCredentialsResponseMap = { [SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse; [SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse; [SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse; + [SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse; + [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse; }; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts new file mode 100644 index 000000000..b8d2ade2b --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts @@ -0,0 +1,37 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TLdapPasswordRotation = TSecretRotationV2Base & { + type: SecretRotation.LdapPassword; + parameters: { + dn: string; + }; + secretsMapping: { + dn: string; + password: string; + }; +}; + +export type TLdapPasswordRotationGeneratedCredentials = { + dn: string; + password: string; +}; + +export type TLdapPasswordRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.LdapPassword, + TLdapPasswordRotationGeneratedCredentials + >; + +export type TLdapPasswordRotationOption = { + name: string; + type: SecretRotation.LdapPassword; + connection: AppConnection.LDAP; + template: { + secretsMapping: TLdapPasswordRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index b0d3fd7bb..450df773a 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -10,7 +10,8 @@ export enum SecretSync { TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel", - Windmill = "windmill" + Windmill = "windmill", + TeamCity = "teamcity" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 21fda56c7..e9ac538fe 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -10,6 +10,7 @@ import { TDatabricksSync } from "./databricks-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; import { THumanitecSync } from "./humanitec-sync"; +import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TVercelSync } from "./vercel-sync"; import { TWindmillSync } from "./windmill-sync"; @@ -32,7 +33,8 @@ export type TSecretSync = | TTerraformCloudSync | TCamundaSync | TVercelSync - | TWindmillSync; + | TWindmillSync + | TTeamCitySync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/hooks/api/secretSyncs/types/teamcity-sync.ts b/frontend/src/hooks/api/secretSyncs/types/teamcity-sync.ts new file mode 100644 index 000000000..17f218d06 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/teamcity-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TTeamCitySync = TRootSecretSync & { + destination: SecretSync.TeamCity; + destinationConfig: { + project: string; + buildConfig?: string; + }; + connection: { + app: AppConnection.TeamCity; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index b9d9f159b..0774275f9 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -1,6 +1,7 @@ export { useAddUserToWsE2EE, useAddUserToWsNonE2EE, + useRevokeMySessionById, useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "./mutation"; diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index c5cf6c27b..1b873b31c 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -171,3 +171,16 @@ export const useResendOrgMemberInvitation = () => { } }); }; + +export const useRevokeMySessionById = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (sessionId: string) => { + const { data } = await apiRequest.delete(`/api/v2/users/me/sessions/${sessionId}`); + return data; + }, + onSuccess() { + queryClient.invalidateQueries({ queryKey: userKeys.mySessions }); + } + }); +}; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index c0f5f027d..c4defb68d 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -4,7 +4,8 @@ export { useLeaveProject, useMigrateProjectToV3, useRequestProjectAccess, - useUpdateGroupWorkspaceRole + useUpdateGroupWorkspaceRole, + useUpdateProjectSshConfig } from "./mutations"; export { useAddIdentityToWorkspace, @@ -14,6 +15,7 @@ export { useDeleteUserFromWorkspace, useDeleteWorkspace, useDeleteWsEnvironment, + useGetProjectSshConfig, useGetUpgradeProjectStatus, useGetUserWorkspaceMemberships, useGetUserWorkspaces, diff --git a/frontend/src/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/workspace/mutations.tsx index 56f83f601..ea7376d3d 100644 --- a/frontend/src/hooks/api/workspace/mutations.tsx +++ b/frontend/src/hooks/api/workspace/mutations.tsx @@ -4,7 +4,11 @@ import { apiRequest } from "@app/config/request"; import { userKeys } from "../users/query-keys"; import { workspaceKeys } from "./query-keys"; -import { TUpdateWorkspaceGroupRoleDTO } from "./types"; +import { + TProjectSshConfig, + TUpdateProjectSshConfigDTO, + TUpdateWorkspaceGroupRoleDTO +} from "./types"; export const useAddGroupToWorkspace = () => { const queryClient = useQueryClient(); @@ -117,3 +121,20 @@ export const useRequestProjectAccess = () => { } }); }; + +export const useUpdateProjectSshConfig = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ projectId, defaultUserSshCaId, defaultHostSshCaId }) => { + return apiRequest.patch(`/api/v1/workspace/${projectId}/ssh-config`, { + defaultUserSshCaId, + defaultHostSshCaId + }); + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getProjectSshConfig(projectId) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index ca8feb6b6..0a2bf491b 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -34,6 +34,7 @@ import { TListProjectIdentitiesDTO, ToggleAutoCapitalizationDTO, ToggleDeleteProjectProtectionDTO, + TProjectSshConfig, TSearchProjectsDTO, TUpdateWorkspaceIdentityRoleDTO, TUpdateWorkspaceUserRoleDTO, @@ -430,9 +431,13 @@ export const useDeleteWsEnvironment = () => { }); }; -export const useGetWorkspaceUsers = (workspaceId: string, includeGroupMembers?: boolean) => { +export const useGetWorkspaceUsers = ( + workspaceId: string, + includeGroupMembers?: boolean, + roles?: string[] +) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceUsers(workspaceId), + queryKey: workspaceKeys.getWorkspaceUsers(workspaceId, includeGroupMembers, roles), queryFn: async () => { const { data: { users } @@ -440,7 +445,11 @@ export const useGetWorkspaceUsers = (workspaceId: string, includeGroupMembers?: `/api/v1/workspace/${workspaceId}/users`, { params: { - includeGroupMembers + includeGroupMembers, + roles: + roles && roles.length > 0 + ? roles.map((role) => encodeURIComponent(role)).join(",") + : undefined } } ); @@ -887,3 +896,17 @@ export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: strin enabled: Boolean(workspaceId) }); }; + +export const useGetProjectSshConfig = (projectId: string) => { + return useQuery({ + queryKey: workspaceKeys.getProjectSshConfig(projectId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/workspace/${projectId}/ssh-config` + ); + + return data; + }, + enabled: Boolean(projectId) + }); +}; diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index 539ed2ac7..05e9c7588 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -15,7 +15,8 @@ export const workspaceKeys = { type ? ["workspaces", { type }] : (["workspaces"] as const), getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }, "workspace-audit-logs"] as const, - getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const, + getWorkspaceUsers: (workspaceId: string, includeGroupMembers?: boolean, roles?: string[]) => + [{ workspaceId, includeGroupMembers, roles }, "workspace-users"] as const, getWorkspaceUserDetails: (workspaceId: string, membershipId: string) => [{ workspaceId, membershipId }, "workspace-user-details"] as const, getWorkspaceIdentityMemberships: (workspaceId: string) => @@ -69,5 +70,6 @@ export const workspaceKeys = { projectId: string; }) => [...workspaceKeys.allWorkspaceSshCertificates(projectId), { offset, limit }] as const, getWorkspaceSshCertificateTemplates: (projectId: string) => - [{ projectId }, "workspace-ssh-certificate-templates"] as const + [{ projectId }, "workspace-ssh-certificate-templates"] as const, + getProjectSshConfig: (projectId: string) => [{ projectId }, "project-ssh-config"] as const }; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 814a920c2..ddcf383fb 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -184,3 +184,18 @@ export type TSearchProjectsDTO = { orderBy?: ProjectIdentityOrderBy; orderDirection?: OrderByDirection; }; + +export type TProjectSshConfig = { + id: string; + createdAt: string; + updatedAt: string; + projectId: string; + defaultUserSshCaId: string | null; + defaultHostSshCaId: string | null; +}; + +export type TUpdateProjectSshConfigDTO = { + projectId: string; + defaultUserSshCaId?: string; + defaultHostSshCaId?: string; +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index e31018334..ba0b4f194 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -57,7 +57,7 @@ import { MenuIconButton } from "../MenuIconButton"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; const getPlan = (subscription: SubscriptionPlan) => { - if (subscription.dynamicSecret) return "Enterprise Plan"; + if (subscription.groups) return "Enterprise Plan"; if (subscription.pitRecovery) return "Pro Plan"; return "Free Plan"; }; diff --git a/frontend/src/layouts/OrganizationLayout/components/SidebarHeader/SidebarHeader.tsx b/frontend/src/layouts/OrganizationLayout/components/SidebarHeader/SidebarHeader.tsx index 8c16668f3..e5ca023aa 100644 --- a/frontend/src/layouts/OrganizationLayout/components/SidebarHeader/SidebarHeader.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/SidebarHeader/SidebarHeader.tsx @@ -2,7 +2,7 @@ import { useOrganization, useSubscription } from "@app/context"; import { SubscriptionPlan } from "@app/hooks/api/types"; const getPlan = (subscription: SubscriptionPlan) => { - if (subscription.dynamicSecret) return "Enterprise Plan"; + if (subscription.groups) return "Enterprise Plan"; if (subscription.pitRecovery) return "Pro Plan"; return "Free Plan"; }; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index b32c24b45..10233802a 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet, useRouterState } from "@tanstack/react-router"; import { motion } from "framer-motion"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { BreadcrumbContainer, Menu, @@ -11,7 +12,13 @@ import { MenuItem, TBreadcrumbFormat } from "@app/components/v2"; -import { useProjectPermission, useSubscription, useWorkspace } from "@app/context"; +import { + useProjectPermission + ProjectPermissionActions, + ProjectPermissionSub, + useSubscription, + useWorkspace +} from "@app/context"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount, @@ -33,6 +40,7 @@ export const ProjectLayout = () => { const { assumedPrivilegeDetails } = useProjectPermission(); const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; + const { subscription } = useSubscription(); const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager; const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager; @@ -49,7 +57,6 @@ export const ProjectLayout = () => { }); // we only show the secret rotations v1 tab if they have existing rotations - const { subscription } = useSubscription(); const { data: secretRotations } = useGetSecretRotations({ workspaceId, options: { @@ -95,18 +102,46 @@ export const ProjectLayout = () => { )} {isCertManager && ( - - {({ isActive }) => ( - - Overview - - )} - + <> + + {({ isActive }) => ( + + Certificates + + )} + + + {({ isActive }) => ( + + Certificate Authorities + + )} + + + {({ isActive }) => ( + + Alerting + + )} + + )} {isCmek && ( { )} */} - {/* - {({ isActive }) => ( - - Certificate Authorities - - )} - */} + {(isAllowed) => + isAllowed && ( + + {({ isActive }) => ( + + Certificate Authorities + + )} + + ) + } + )} {isSecretManager && ( diff --git a/frontend/src/lib/fn/string.ts b/frontend/src/lib/fn/string.ts index f4c4a43db..6b2842701 100644 --- a/frontend/src/lib/fn/string.ts +++ b/frontend/src/lib/fn/string.ts @@ -11,3 +11,65 @@ export const formatReservedPaths = (secretPath: string) => { export const camelCaseToSpaces = (input: string) => { return input.replace(/([a-z])([A-Z])/g, "$1 $2"); }; + +export const formatSessionUserAgent = (userAgent: string) => { + const result = { + os: "Unknown", + browser: "Unknown", + device: "Desktop" + }; + + // Operating System detection + if (userAgent.includes("Windows")) { + result.os = "Windows"; + } else if ( + userAgent.includes("Mac OS") || + userAgent.includes("Macintosh") || + userAgent.includes("macOS") + ) { + result.os = "macOS"; + } else if (userAgent.includes("Linux") && !userAgent.includes("Android")) { + result.os = "Linux"; + } else if (userAgent.includes("Android")) { + result.os = "Android"; + result.device = "Mobile"; + } else if ( + userAgent.includes("iOS") || + userAgent.includes("iPhone") || + userAgent.includes("iPad") + ) { + result.os = "iOS"; + result.device = userAgent.includes("iPad") ? "Tablet" : "Mobile"; + } + + // Browser detection + if (userAgent.includes("Firefox/")) { + result.browser = "Firefox"; + } else if (userAgent.includes("Edge/") || userAgent.includes("Edg/")) { + result.browser = "Edge"; + } else if (userAgent.includes("Brave/") || userAgent.includes("Brave ")) { + result.browser = "Brave"; + } else if ( + userAgent.includes("Chrome/") && + !userAgent.includes("Chromium/") && + !userAgent.includes("Edg/") + ) { + result.browser = "Chrome"; + } else if ( + userAgent.includes("Safari/") && + !userAgent.includes("Chrome/") && + !userAgent.includes("Chromium/") + ) { + result.browser = "Safari"; + } else if (userAgent.includes("Opera/") || userAgent.includes("OPR/")) { + result.browser = "Opera"; + } else if (userAgent.includes("Trident/") || userAgent.includes("MSIE")) { + result.browser = "Internet Explorer"; + } + + if (userAgent.toLowerCase() === "cli") { + result.browser = "CLI"; + } + + return result; +}; diff --git a/frontend/src/pages/cert-manager/AlertingPage/AlertingPage.tsx b/frontend/src/pages/cert-manager/AlertingPage/AlertingPage.tsx new file mode 100644 index 000000000..40e3eb100 --- /dev/null +++ b/frontend/src/pages/cert-manager/AlertingPage/AlertingPage.tsx @@ -0,0 +1,29 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { PageHeader } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; + +import { PkiAlertsSection } from "./components"; + +export const AlertingPage = () => { + const { t } = useTranslation(); + return ( +
+ + {t("common.head-title", { title: "Alerting" })} + +
+ + + + +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertRow.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertRow.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertRow.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertRow.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsSection.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsSection.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsSection.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsTable.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsTable.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsTable.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/PkiAlertsTable.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionSection.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionSection.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionSection.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionTable.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionTable.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/PkiCollectionTable.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/index.tsx b/frontend/src/pages/cert-manager/AlertingPage/components/index.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/components/index.tsx rename to frontend/src/pages/cert-manager/AlertingPage/components/index.tsx diff --git a/frontend/src/pages/cert-manager/AlertingPage/route.tsx b/frontend/src/pages/cert-manager/AlertingPage/route.tsx new file mode 100644 index 000000000..d14e64d94 --- /dev/null +++ b/frontend/src/pages/cert-manager/AlertingPage/route.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AlertingPage } from "./AlertingPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting" +)({ + component: AlertingPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Alerting" + } + ] + }; + } +}); diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index 75d0ca139..9bb6c9096 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -20,9 +20,9 @@ import { useDeleteCa, useGetCaById } from "@app/hooks/api"; import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; -import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; -import { CaModal } from "../CertificatesPage/components/CaTab/components/CaModal"; -import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection"; +import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal"; +import { CaModal } from "../CertificateAuthoritiesPage/components/CaModal"; +import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificateTemplatesSection"; import { CaCertificatesSection, CaCrlsSection, diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx index 00244eb98..aa7540a7f 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx @@ -13,7 +13,7 @@ export const Route = createFileRoute( { label: "Certificate Authorities", link: linkOptions({ - to: "/cert-manager/$projectId/overview", + to: "/cert-manager/$projectId/certificate-authorities", params: { projectId: params.projectId } diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx new file mode 100644 index 000000000..f74ececaf --- /dev/null +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx @@ -0,0 +1,29 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { PageHeader } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; + +import { CaSection } from "./components"; + +export const CertificateAuthoritiesPage = () => { + const { t } = useTranslation(); + return ( +
+ + {t("common.head-title", { title: "Certificate Authorities" })} + +
+ + + + +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaCertModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaCertModal.tsx similarity index 91% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaCertModal.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaCertModal.tsx index 97970d94f..113a88c48 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaCertModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaCertModal.tsx @@ -2,7 +2,7 @@ import { Modal, ModalContent } from "@app/components/v2"; import { useGetCaCert } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; -import { CertificateContent } from "../../CertificatesTab/components/CertificateContent"; +import { CertificateContent } from "../../CertificatesPage/components/CertificateContent"; type Props = { popUp: UsePopUpState<["caCert"]>; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/CaInstallCertModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/CaInstallCertModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/ExternalCaInstallForm.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/InternalCaInstallForm.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/InternalCaInstallForm.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/index.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/index.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/index.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/index.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaModal.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaSection.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaTable.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/index.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/index.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/components/index.tsx rename to frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/index.tsx diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/route.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/route.tsx new file mode 100644 index 000000000..e11496710 --- /dev/null +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/route.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { CertificateAuthoritiesPage } from "./CertificateAuthoritiesPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities" +)({ + component: CertificateAuthoritiesPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Certificate Authorities" + } + ] + }; + } +}); diff --git a/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx b/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx index 0d212e797..c985f313f 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx @@ -2,63 +2,49 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { PageHeader } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; -import { CaTab, CertificatesTab, PkiAlertsTab } from "./components"; - -enum TabSections { - Ca = "certificate-authorities", - Certificates = "certificates", - Alerting = "alerting" -} +import { PkiCollectionSection } from "../AlertingPage/components"; +import { CertificatesSection } from "./components"; export const CertificatesPage = () => { const { t } = useTranslation(); + const { permission } = useProjectPermission(); + + const canAccessPkiColl = permission.can( + ProjectPermissionActions.Read, + ProjectPermissionSub.PkiCollections + ); + const canAccessCerts = permission.can( + ProjectPermissionActions.Read, + ProjectPermissionSub.Certificates + ); + return (
{t("common.head-title", { title: "Certificates" })}
- - - - Certificates - Certificate Authorities - Alerting - - - - - - - - - - - - - - - - - + + {/* If both are false, the section does not render. This is to prevent duplicate banners. */} + {(canAccessCerts || canAccessPkiColl) && ( + + + + )} + + +
); diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/CaTab.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/CaTab.tsx deleted file mode 100644 index aadca4dfe..000000000 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/CaTab.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { motion } from "framer-motion"; - -import { CaSection } from "./components"; - -export const CaTab = () => { - return ( - - - - ); -}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/index.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/index.tsx deleted file mode 100644 index 9e52be028..000000000 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CaTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CaTab } from "./CaTab"; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateCertModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateCertModal.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateContent.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateContent.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateRevocationModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateRevocationModal.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplateEnrollmentModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateEnrollmentModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplateEnrollmentModal.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateEnrollmentModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesSection.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesTable.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesTable.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesTable.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificatesSection.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/CertificatesTab.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/CertificatesTab.tsx deleted file mode 100644 index 0f74920ec..000000000 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/CertificatesTab.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { motion } from "framer-motion"; - -import { PkiCollectionSection } from "../PkiAlertsTab/components"; -// import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection"; -import { CertificatesSection } from "./components"; - -export const CertificatesTab = () => { - return ( - - - {/* */} - - - ); -}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/index.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/index.tsx deleted file mode 100644 index 7854a6f8b..000000000 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CertificatesSection } from "./CertificatesSection"; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/index.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/index.tsx deleted file mode 100644 index 277134d56..000000000 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CertificatesTab } from "./CertificatesTab"; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificatesTable.utils.ts b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.utils.ts similarity index 100% rename from frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificatesTable.utils.ts rename to frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.utils.ts diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx deleted file mode 100644 index 3a5a04345..000000000 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { motion } from "framer-motion"; - -import { PkiAlertsSection } from "./components"; - -export const PkiAlertsTab = () => { - return ( - - - - ); -}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/index.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/index.tsx deleted file mode 100644 index 0acaf13d6..000000000 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/PkiAlertsTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { PkiAlertsTab } from "./PkiAlertsTab"; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/index.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/index.tsx index 851d403a0..7854a6f8b 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/index.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/index.tsx @@ -1,3 +1 @@ -export { CaTab } from "./CaTab"; -export { CertificatesTab } from "./CertificatesTab"; -export { PkiAlertsTab } from "./PkiAlertsTab"; +export { CertificatesSection } from "./CertificatesSection"; diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx index 5cd8bf7c6..073cac6e8 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx @@ -22,7 +22,7 @@ import { PkiItemType } from "@app/hooks/api/pkiCollections/constants"; import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; -import { PkiCollectionModal } from "../CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal"; +import { PkiCollectionModal } from "../AlertingPage/components/PkiCollectionModal"; import { PkiCollectionDetailsSection, PkiCollectionItemsSection } from "./components"; export const PkiCollectionPage = () => { diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx index 4f8744682..81694e025 100644 --- a/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx @@ -1,6 +1,6 @@ import { Modal, ModalContent } from "@app/components/v2"; import { KmipClientCertificate } from "@app/hooks/api/kmip/types"; -import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent"; +import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificateContent"; type Props = { isOpen: boolean; diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 615d03b60..03005ce05 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -1,7 +1,9 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { AxiosError } from "axios"; +import { addSeconds, formatISO } from "date-fns"; import { createNotification } from "@app/components/notifications"; +import { SessionStorageKeys } from "@app/const"; import { ROUTE_PATHS } from "@app/const/routes"; import { userKeys } from "@app/hooks/api"; import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; @@ -24,6 +26,16 @@ export const Route = createFileRoute("/_authenticate")({ title: "Access Restricted", text: " You need to log in to access this page. Please log in to continue." }); + + // persist current URL in session storage so that we can come back to this after successful login + sessionStorage.setItem( + SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 60)), + data: window.location.href + }) + ); + throw redirect({ to: "/login" }); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 05a7eebbc..f47de43dd 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -18,8 +18,10 @@ import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; +import { LdapConnectionForm } from "./LdapConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; +import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; import { WindmillConnectionForm } from "./WindmillConnectionForm"; @@ -89,6 +91,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.LDAP: + return ; + case AppConnection.TeamCity: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -153,6 +159,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.LDAP: + return ; + case AppConnection.TeamCity: + return ; + default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx new file mode 100644 index 000000000..7346f84af --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx @@ -0,0 +1,329 @@ +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tab } from "@headlessui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem, + Switch, + TextArea, + Tooltip +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { DistinguishedNameRegex } from "@app/helpers/string"; +import { + LdapConnectionMethod, + LdapConnectionProvider, + TLdapConnection +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TLdapConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.LDAP) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(LdapConnectionMethod.SimpleBind), + credentials: z.object({ + provider: z.nativeEnum(LdapConnectionProvider), + url: z + .string() + .regex(/^ldaps?:\/\//, 'Must start with "ldaps://" or "ldap://"') + .url() + .trim() + .min(1, "LDAP URL required"), + dn: z + .string() + .trim() + .regex(DistinguishedNameRegex, "Invalid Distinguished Name format") + .min(1, "Distinguished Name (DN) required"), + password: z.string().trim().min(1, "Password required"), + sslRejectUnauthorized: z.boolean(), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + }) + }) +]); + +type FormData = z.infer; + +export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.LDAP, + method: LdapConnectionMethod.SimpleBind, + credentials: { + provider: LdapConnectionProvider.ActiveDirectory, + url: "", + dn: "", + password: "", + sslRejectUnauthorized: true, + sslCertificate: undefined + } + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty }, + watch + } = form; + + const selectedProvider = watch("credentials.provider"); + const sslEnabled = watch("credentials.url")?.startsWith("ldaps://") ?? false; + + return ( + +
{ + setSelectedTabIndex(0); + handleSubmit(onSubmit)(e); + }} + > + {!isUpdate && } +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + + + `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + Configuration + + + `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + SSL ({sslEnabled ? "Enabled" : "Disabled"}) + + + {selectedTabIndex === 1 && ( +
Requires ldaps:// URL
+ )} + + + ( + + + + )} + /> +
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+
+ + ( + +