diff --git a/backend/package-lock.json b/backend/package-lock.json index be6137424..165323a08 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -33,6 +33,7 @@ "@infisical/quic": "^1.0.8", "@node-saml/passport-saml": "^5.0.1", "@octokit/auth-app": "^7.1.1", + "@octokit/plugin-paginate-graphql": "^5.2.4", "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", @@ -7245,47 +7246,247 @@ } }, "node_modules/@octokit/core": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz", - "integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", + "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", + "license": "MIT", + "peer": true, "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.0.0", - "@octokit/request": "^8.0.2", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "before-after-hook": "^2.2.0", + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/auth-token": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "license": "MIT", + "peer": true + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", + "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/openapi-types": "^25.0.0" + } + }, + "node_modules/@octokit/core/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@octokit/core/node_modules/universal-user-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "license": "ISC", + "peer": true + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/endpoint": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", - "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 18" + "@octokit/openapi-types": "^24.2.0" } }, "node_modules/@octokit/graphql": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", - "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "license": "MIT", + "peer": true, "dependencies": { - "@octokit/request": "^8.0.1", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, + "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "license": "MIT", + "peer": true + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", + "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/openapi-types": "^25.0.0" + } + }, + "node_modules/@octokit/graphql/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@octokit/graphql/node_modules/universal-user-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "license": "ISC", + "peer": true + }, "node_modules/@octokit/oauth-authorization-url": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", @@ -7380,6 +7581,18 @@ "node": ">= 18" } }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.4.tgz", + "integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, "node_modules/@octokit/plugin-paginate-rest": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", @@ -7461,28 +7674,14 @@ "@octokit/openapi-types": "^18.0.0" } }, - "node_modules/@octokit/plugin-throttling": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.1.3.tgz", - "integrity": "sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==", - "dependencies": { - "@octokit/types": "^12.2.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "^5.0.0" - } - }, "node_modules/@octokit/request": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", - "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.1", - "@octokit/request-error": "^5.1.0", + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" }, @@ -7491,9 +7690,10 @@ } }, "node_modules/@octokit/request-error": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", - "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", @@ -7543,6 +7743,59 @@ "node": ">= 18" } }, + "node_modules/@octokit/rest/node_modules/@octokit/core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/rest/node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, "node_modules/@octokit/types": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", @@ -11654,9 +11907,11 @@ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "license": "Apache-2.0", + "peer": true }, "node_modules/big-integer": { "version": "1.6.52", @@ -19667,6 +19922,62 @@ "node": ">=18" } }, + "node_modules/probot/node_modules/@octokit/core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/probot/node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/probot/node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/probot/node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/probot/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, "node_modules/probot/node_modules/@octokit/plugin-retry": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", @@ -19683,6 +19994,28 @@ "@octokit/core": ">=5" } }, + "node_modules/probot/node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/probot/node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, "node_modules/probot/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index b2c0d751a..680cf40aa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -150,6 +150,7 @@ "@infisical/quic": "^1.0.8", "@node-saml/passport-saml": "^5.0.1", "@octokit/auth-app": "^7.1.1", + "@octokit/plugin-paginate-graphql": "^5.2.4", "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 185463f81..5b7105f13 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -15,6 +15,7 @@ import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dy import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; @@ -140,6 +141,7 @@ declare module "fastify" { passportUser: { isUserCompleted: boolean; providerAuthToken: string; + externalProviderAccessToken?: string; }; kmipUser: { projectId: string; @@ -244,6 +246,7 @@ declare module "fastify" { gateway: TGatewayServiceFactory; secretRotationV2: TSecretRotationV2ServiceFactory; assumePrivileges: TAssumePrivilegeServiceFactory; + githubOrgSync: TGithubOrgSyncServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index dbb302da1..091199938 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -83,6 +83,9 @@ import { TGitAppOrg, TGitAppOrgInsert, TGitAppOrgUpdate, + TGithubOrgSyncConfigs, + TGithubOrgSyncConfigsInsert, + TGithubOrgSyncConfigsUpdate, TGroupProjectMembershipRoles, TGroupProjectMembershipRolesInsert, TGroupProjectMembershipRolesUpdate, @@ -1004,5 +1007,10 @@ declare module "knex/types/tables" { TSecretReminderRecipientsInsert, TSecretReminderRecipientsUpdate >; + [TableName.GithubOrgSyncConfig]: KnexOriginal.CompositeTableType< + TGithubOrgSyncConfigs, + TGithubOrgSyncConfigsInsert, + TGithubOrgSyncConfigsUpdate + >; } } diff --git a/backend/src/db/migrations/20250426075943_github-org-sync-config.ts b/backend/src/db/migrations/20250426075943_github-org-sync-config.ts new file mode 100644 index 000000000..9b0c936b3 --- /dev/null +++ b/backend/src/db/migrations/20250426075943_github-org-sync-config.ts @@ -0,0 +1,26 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const hasTable = await knex.schema.hasTable(TableName.GithubOrgSyncConfig); + if (!hasTable) { + await knex.schema.createTable(TableName.GithubOrgSyncConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("githubOrgName").notNullable(); + t.boolean("isActive").defaultTo(false); + t.binary("encryptedGithubOrgAccessToken"); + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.GithubOrgSyncConfig); + await dropOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig); +} diff --git a/backend/src/db/schemas/github-org-sync-configs.ts b/backend/src/db/schemas/github-org-sync-configs.ts new file mode 100644 index 000000000..9e57b8a30 --- /dev/null +++ b/backend/src/db/schemas/github-org-sync-configs.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const GithubOrgSyncConfigsSchema = z.object({ + id: z.string().uuid(), + githubOrgName: z.string(), + isActive: z.boolean().default(false).nullable().optional(), + encryptedGithubOrgAccessToken: zodBuffer.nullable().optional(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TGithubOrgSyncConfigs = z.infer; +export type TGithubOrgSyncConfigsInsert = Omit, TImmutableDBKeys>; +export type TGithubOrgSyncConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 8543417cf..7ccd71376 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -25,6 +25,7 @@ export * from "./external-kms"; export * from "./gateways"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; +export * from "./github-org-sync-configs"; export * from "./group-project-membership-roles"; export * from "./group-project-memberships"; export * from "./groups"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index be80d5ca4..dd23c26da 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -147,7 +147,8 @@ export enum TableName { KmipClientCertificates = "kmip_client_certificates", SecretRotationV2 = "secret_rotations_v2", SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings", - SecretReminderRecipients = "secret_reminder_recipients" + SecretReminderRecipients = "secret_reminder_recipients", + GithubOrgSyncConfig = "github_org_sync_configs" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; diff --git a/backend/src/ee/routes/v1/github-org-sync-router.ts b/backend/src/ee/routes/v1/github-org-sync-router.ts new file mode 100644 index 000000000..3f33a5d8f --- /dev/null +++ b/backend/src/ee/routes/v1/github-org-sync-router.ts @@ -0,0 +1,129 @@ +import { z } from "zod"; + +import { GithubOrgSyncConfigsSchema } from "@app/db/schemas"; +import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string"; +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"; + +const SanitizedGithubOrgSyncSchema = GithubOrgSyncConfigsSchema.pick({ + isActive: true, + id: true, + createdAt: true, + updatedAt: true, + orgId: true, + githubOrgName: true +}); + +const githubOrgNameValidator = zodValidateCharacters([CharacterType.AlphaNumeric, CharacterType.Hyphen]); +export const registerGithubOrgSyncRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z.object({ + githubOrgName: githubOrgNameValidator(z.string().trim(), "GitHub Org Name"), + githubOrgAccessToken: z.string().trim().max(1000).optional(), + isActive: z.boolean().default(false) + }), + response: { + 200: z.object({ + githubOrgSyncConfig: SanitizedGithubOrgSyncSchema + }) + } + }, + handler: async (req) => { + const githubOrgSyncConfig = await server.services.githubOrgSync.createGithubOrgSync({ + orgPermission: req.permission, + githubOrgName: req.body.githubOrgName, + githubOrgAccessToken: req.body.githubOrgAccessToken, + isActive: req.body.isActive + }); + + return { githubOrgSyncConfig }; + } + }); + + server.route({ + url: "/", + method: "PATCH", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z + .object({ + githubOrgName: githubOrgNameValidator(z.string().trim(), "GitHub Org Name"), + githubOrgAccessToken: z.string().trim().max(1000), + isActive: z.boolean() + }) + .partial(), + response: { + 200: z.object({ + githubOrgSyncConfig: SanitizedGithubOrgSyncSchema + }) + } + }, + handler: async (req) => { + const githubOrgSyncConfig = await server.services.githubOrgSync.updateGithubOrgSync({ + orgPermission: req.permission, + githubOrgName: req.body.githubOrgName, + githubOrgAccessToken: req.body.githubOrgAccessToken, + isActive: req.body.isActive + }); + + return { githubOrgSyncConfig }; + } + }); + + server.route({ + url: "/", + method: "DELETE", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + response: { + 200: z.object({ + githubOrgSyncConfig: SanitizedGithubOrgSyncSchema + }) + } + }, + handler: async (req) => { + const githubOrgSyncConfig = await server.services.githubOrgSync.deleteGithubOrgSync({ + orgPermission: req.permission + }); + + return { githubOrgSyncConfig }; + } + }); + + server.route({ + url: "/", + method: "GET", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + response: { + 200: z.object({ + githubOrgSyncConfig: SanitizedGithubOrgSyncSchema + }) + } + }, + handler: async (req) => { + const githubOrgSyncConfig = await server.services.githubOrgSync.getGithubOrgSync({ + orgPermission: req.permission + }); + + return { githubOrgSyncConfig }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 9c474ef38..a88ebf258 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -9,6 +9,7 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router" import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; import { registerGatewayRouter } from "./gateway-router"; +import { registerGithubOrgSyncRouter } from "./github-org-sync-router"; import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerKmipRouter } from "./kmip-router"; @@ -72,6 +73,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { ); await server.register(registerGatewayRouter, { prefix: "/gateways" }); + await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" }); await server.register( async (pkiRouter) => { diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-dal.ts b/backend/src/ee/services/github-org-sync/github-org-sync-dal.ts new file mode 100644 index 000000000..cda843b57 --- /dev/null +++ b/backend/src/ee/services/github-org-sync/github-org-sync-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TGithubOrgSyncDALFactory = ReturnType; + +export const githubOrgSyncDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.GithubOrgSyncConfig); + return orm; +}; diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts new file mode 100644 index 000000000..22a078399 --- /dev/null +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -0,0 +1,354 @@ +import { ForbiddenError } from "@casl/ability"; +import { Octokit } from "@octokit/core"; +import { paginateGraphQL } from "@octokit/plugin-paginate-graphql"; +import { Octokit as OctokitRest } from "@octokit/rest"; + +import { OrgMembershipRole } from "@app/db/schemas"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TGroupDALFactory } from "../group/group-dal"; +import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal"; +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal"; +import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types"; + +const OctokitWithPlugin = Octokit.plugin(paginateGraphQL); + +type TGithubOrgSyncServiceFactoryDep = { + githubOrgSyncDAL: TGithubOrgSyncDALFactory; + permissionService: Pick; + kmsService: Pick; + userGroupMembershipDAL: Pick< + TUserGroupMembershipDALFactory, + "findGroupMembershipsByUserIdInOrg" | "insertMany" | "delete" + >; + groupDAL: Pick; + licenseService: Pick; +}; + +export type TGithubOrgSyncServiceFactory = ReturnType; + +export const githubOrgSyncServiceFactory = ({ + githubOrgSyncDAL, + permissionService, + kmsService, + userGroupMembershipDAL, + groupDAL, + licenseService +}: TGithubOrgSyncServiceFactoryDep) => { + const createGithubOrgSync = async ({ + githubOrgName, + orgPermission, + githubOrgAccessToken, + isActive + }: TCreateGithubOrgSyncDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync); + const plan = await licenseService.getPlan(orgPermission.orgId); + if (!plan.githubOrgSync) { + throw new BadRequestError({ + message: + "Failed to create github organization team sync due to plan restriction. Upgrade plan to create github organization sync." + }); + } + + const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId }); + if (existingConfig) + throw new BadRequestError({ + message: `Organization ${orgPermission.orgId} already has GitHub Organization sync config.` + }); + + const octokit = new OctokitRest({ + auth: githubOrgAccessToken, + request: { + signal: AbortSignal.timeout(5000) + } + }); + const { data } = await octokit.rest.orgs.get({ + org: githubOrgName + }); + if (data.login.toLowerCase() !== githubOrgName.toLowerCase()) + throw new BadRequestError({ message: "Invalid GitHub organisation" }); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgPermission.orgId + }); + + const config = await githubOrgSyncDAL.create({ + orgId: orgPermission.orgId, + githubOrgName, + isActive, + encryptedGithubOrgAccessToken: githubOrgAccessToken + ? encryptor({ plainText: Buffer.from(githubOrgAccessToken) }).cipherTextBlob + : null + }); + + return config; + }; + + const updateGithubOrgSync = async ({ + githubOrgName, + orgPermission, + githubOrgAccessToken, + isActive + }: TUpdateGithubOrgSyncDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync); + const plan = await licenseService.getPlan(orgPermission.orgId); + if (!plan.githubOrgSync) { + throw new BadRequestError({ + message: + "Failed to update github organization team sync due to plan restriction. Upgrade plan to update github organization sync." + }); + } + + const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId }); + if (!existingConfig) + throw new BadRequestError({ + message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.` + }); + + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgPermission.orgId + }); + const newData = { + githubOrgName: githubOrgName || existingConfig.githubOrgName, + githubOrgAccessToken: + githubOrgAccessToken || + (existingConfig.encryptedGithubOrgAccessToken + ? decryptor({ cipherTextBlob: existingConfig.encryptedGithubOrgAccessToken }).toString() + : null) + }; + + if (githubOrgName || githubOrgAccessToken) { + const octokit = new OctokitRest({ + auth: newData.githubOrgAccessToken, + request: { + signal: AbortSignal.timeout(5000) + } + }); + const { data } = await octokit.rest.orgs.get({ + org: newData.githubOrgName + }); + + if (data.login.toLowerCase() !== newData.githubOrgName.toLowerCase()) + throw new BadRequestError({ message: "Invalid GitHub organisation" }); + } + + const config = await githubOrgSyncDAL.updateById(existingConfig.id, { + orgId: orgPermission.orgId, + githubOrgName: newData.githubOrgName, + isActive, + encryptedGithubOrgAccessToken: newData.githubOrgAccessToken + ? encryptor({ plainText: Buffer.from(newData.githubOrgAccessToken) }).cipherTextBlob + : null + }); + + return config; + }; + + const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync); + + const plan = await licenseService.getPlan(orgPermission.orgId); + if (!plan.githubOrgSync) { + throw new BadRequestError({ + message: + "Failed to delete github organization team sync due to plan restriction. Upgrade plan to delete github organization sync." + }); + } + + const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId }); + if (!existingConfig) + throw new BadRequestError({ + message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.` + }); + + const config = await githubOrgSyncDAL.deleteById(existingConfig.id); + + return config; + }; + + const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); + + const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId }); + if (!existingConfig) + throw new NotFoundError({ + message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.` + }); + + return existingConfig; + }; + + const syncUserGroups = async (orgId: string, userId: string, accessToken: string) => { + const config = await githubOrgSyncDAL.findOne({ orgId }); + if (!config || !config?.isActive) return; + + const infisicalUserGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(userId, orgId); + const infisicalUserGroupSet = new Set(infisicalUserGroups.map((el) => el.groupName)); + + const octoRest = new OctokitRest({ + auth: accessToken, + request: { + signal: AbortSignal.timeout(5000) + } + }); + const { data: userOrgMembershipDetails } = await octoRest.rest.orgs + .getMembershipForAuthenticatedUser({ + org: config.githubOrgName + }) + .catch((err) => { + logger.error(err, "User not part of GitHub synced organization"); + throw new BadRequestError({ message: "User not part of GitHub synced organization" }); + }); + const username = userOrgMembershipDetails?.user?.login; + if (!username) throw new BadRequestError({ message: "User not part of GitHub synced organization" }); + + const octokit = new OctokitWithPlugin({ + auth: accessToken, + request: { + signal: AbortSignal.timeout(5000) + } + }); + const data = await octokit.graphql + .paginate<{ + organization: { teams: { totalCount: number; edges: { node: { name: string; description: string } }[] } }; + }>( + ` + query orgTeams($cursor: String,$org: String!, $username: String!){ + organization(login: $org) { + teams(first: 100, userLogins: [$username], after: $cursor) { + totalCount + edges { + node { + name + description + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `, + { + org: config.githubOrgName, + username + } + ) + .catch((err) => { + if ((err as Error)?.message?.includes("Although you appear to have the correct authorization credential")) { + throw new BadRequestError({ + message: + "Please check your organization have approved Infisical Oauth application. For more info: https://infisical.com/docs/documentation/platform/github-org-sync#troubleshooting" + }); + } + throw new BadRequestError({ message: (err as Error)?.message }); + }); + + const { + organization: { teams } + } = data; + const githubUserTeams = teams?.edges?.map((el) => el.node.name.toLowerCase()) || []; + const githubUserTeamSet = new Set(githubUserTeams); + const githubUserTeamOnInfisical = await groupDAL.find({ orgId, $in: { name: githubUserTeams } }); + const githubUserTeamOnInfisicalGroupByName = groupBy(githubUserTeamOnInfisical, (i) => i.name); + + const newTeams = githubUserTeams.filter( + (el) => !infisicalUserGroupSet.has(el) && !Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el) + ); + const updateTeams = githubUserTeams.filter( + (el) => !infisicalUserGroupSet.has(el) && Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el) + ); + const removeFromTeams = infisicalUserGroups.filter((el) => !githubUserTeamSet.has(el.groupName)); + + if (newTeams.length || updateTeams.length || removeFromTeams.length) { + await groupDAL.transaction(async (tx) => { + if (newTeams.length) { + const newGroups = await groupDAL.insertMany( + newTeams.map((newGroupName) => ({ + name: newGroupName, + role: OrgMembershipRole.Member, + slug: newGroupName, + orgId + })), + tx + ); + await userGroupMembershipDAL.insertMany( + newGroups.map((el) => ({ + groupId: el.id, + userId + })), + tx + ); + } + + if (updateTeams.length) { + await userGroupMembershipDAL.insertMany( + updateTeams.map((el) => ({ + groupId: githubUserTeamOnInfisicalGroupByName[el][0].id, + userId + })), + tx + ); + } + + if (removeFromTeams.length) { + await userGroupMembershipDAL.delete( + { userId, $in: { groupId: removeFromTeams.map((el) => el.groupId) } }, + tx + ); + } + }); + } + }; + + return { + createGithubOrgSync, + updateGithubOrgSync, + deleteGithubOrgSync, + getGithubOrgSync, + syncUserGroups + }; +}; diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-types.ts b/backend/src/ee/services/github-org-sync/github-org-sync-types.ts new file mode 100644 index 000000000..e1df71e82 --- /dev/null +++ b/backend/src/ee/services/github-org-sync/github-org-sync-types.ts @@ -0,0 +1,23 @@ +import { OrgServiceActor } from "@app/lib/types"; + +export interface TCreateGithubOrgSyncDTO { + orgPermission: OrgServiceActor; + githubOrgName: string; + githubOrgAccessToken?: string; + isActive?: boolean; +} + +export interface TUpdateGithubOrgSyncDTO { + orgPermission: OrgServiceActor; + githubOrgName?: string; + githubOrgAccessToken?: string; + isActive?: boolean; +} + +export interface TDeleteGithubOrgSyncDTO { + orgPermission: OrgServiceActor; +} + +export interface TGetGithubOrgSyncDTO { + orgPermission: OrgServiceActor; +} diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 3f4af174b..548f6e82b 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -22,6 +22,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ pitRecovery: false, ipAllowlisting: false, rbac: false, + githubOrgSync: false, customRateLimits: false, customAlerts: false, secretAccessInsights: false, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index c2bf42e2e..6f0d82344 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -45,6 +45,7 @@ export type TFeatureSet = { auditLogsRetentionDays: 0; auditLogStreams: false; auditLogStreamLimit: 3; + githubOrgSync: false; samlSSO: false; hsm: false; oidcSSO: false; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 17b4e7f6c..7026899c7 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -74,6 +74,7 @@ export enum OrgPermissionSubjects { IncidentAccount = "incident-contact", Sso = "sso", Scim = "scim", + GithubOrgSync = "github-org-sync", Ldap = "ldap", Groups = "groups", Billing = "billing", @@ -101,6 +102,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Scim] + | [OrgPermissionActions, OrgPermissionSubjects.GithubOrgSync] | [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] @@ -165,6 +167,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") }), + z.object({ + subject: z.literal(OrgPermissionSubjects.GithubOrgSync).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), z.object({ subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") @@ -273,6 +279,11 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); + can(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); + can(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); diff --git a/backend/src/lib/config/const.ts b/backend/src/lib/config/const.ts new file mode 100644 index 000000000..41038112d --- /dev/null +++ b/backend/src/lib/config/const.ts @@ -0,0 +1 @@ +export const INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN = "x-infisical-github-auth-access-token"; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c0aa5641d..8ceeba648 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -33,6 +33,8 @@ import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal"; +import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; +import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -421,6 +423,7 @@ export const registerRoutes = async ( const gatewayDAL = gatewayDALFactory(db); const projectGatewayDAL = projectGatewayDALFactory(db); const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db); + const githubOrgSyncDAL = githubOrgSyncDALFactory(db); const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL); @@ -558,6 +561,15 @@ export const registerRoutes = async ( externalGroupOrgRoleMappingDAL }); + const githubOrgSyncConfigService = githubOrgSyncServiceFactory({ + licenseService, + githubOrgSyncDAL, + kmsService, + permissionService, + groupDAL, + userGroupMembershipDAL + }); + const ldapService = ldapConfigServiceFactory({ ldapConfigDAL, ldapGroupMapDAL, @@ -1689,7 +1701,8 @@ export const registerRoutes = async ( kmipOperation: kmipOperationService, gateway: gatewayService, secretRotationV2: secretRotationV2Service, - assumePrivileges: assumePrivilegeService + assumePrivileges: assumePrivilegeService, + githubOrgSync: githubOrgSyncConfigService }); const cronJobs: CronJob[] = []; @@ -1750,30 +1763,6 @@ export const registerRoutes = async ( logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`); - // try { - // await db.raw("SELECT NOW()"); - // } catch (err) { - // logger.error("Health check: database connection failed", err); - // return reply.code(503).send({ - // date: new Date(), - // message: "Service unavailable" - // }); - // } - - // if (cfg.isRedisConfigured) { - // const redis = new Redis(cfg.REDIS_URL); - // try { - // await redis.ping(); - // redis.disconnect(); - // } catch (err) { - // logger.error("Health check: redis connection failed", err); - // return reply.code(503).send({ - // date: new Date(), - // message: "Service unavailable" - // }); - // } - // } - return { date: new Date(), message: "Ok", diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index a222ab172..5c04f5d1b 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -14,9 +14,11 @@ import { Strategy as GitLabStrategy } from "passport-gitlab2"; import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { z } from "zod"; +import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { ms } from "@app/lib/ms"; import { fetchGithubEmails } from "@app/lib/requests/github"; import { authRateLimit } from "@app/server/config/rateLimiter"; import { AuthMethod } from "@app/services/auth/auth-type"; @@ -95,7 +97,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string, clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`, - scope: ["user:email"], + scope: ["user:email", "read:org"], // akhilmhdh: because the ts type for this is outdated by the maintainer state: true as unknown as string }, @@ -113,7 +115,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { authMethod: AuthMethod.GITHUB, callbackPort }); - return cb(null, { isUserCompleted, providerAuthToken }); + return cb(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken }); } catch (error) { logger.error(error); cb(error as Error, false); @@ -290,6 +292,17 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { }) as any, handler: async (req, res) => { await req.session.destroy(); + + if (req.passportUser.externalProviderAccessToken) { + void res.cookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, req.passportUser.externalProviderAccessToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED, + expires: new Date(Date.now() + ms(appCfg.JWT_PROVIDER_AUTH_LIFETIME)) + }); + } + if (req.passportUser.isUserCompleted) { return res.redirect( `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index af2d97b8e..91df68e16 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const"; import { getConfig } from "@app/lib/config/env"; import { authRateLimit } from "@app/server/config/rateLimiter"; @@ -70,6 +71,21 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }; } + const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN]; + if (githubOauthAccessToken) { + await server.services.githubOrgSync + .syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken) + .finally(() => { + void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: cfg.HTTPS_ENABLED, + maxAge: 0 + }); + }); + } + void res.setCookie("jid", tokens.refresh, { httpOnly: true, path: "/", diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index bc9c4afa3..0f8ba5176 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -476,6 +476,7 @@ export const authLoginServiceFactory = ({ return { ...tokens, + user, isMfaEnabled: false }; }; @@ -784,7 +785,7 @@ export const authLoginServiceFactory = ({ organizationId }); - return { token, isMfaEnabled: false, user: userEnc } as const; + return { token, isMfaEnabled: false, user: userEnc, decodedProviderToken } as const; }; /* diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index f77becb02..46c5ed2bd 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -177,6 +177,7 @@ export const deleteGithubSecrets = async ({ selected_repositories_url?: string | undefined; } + // @ts-expect-error just octokit ts compatiability issue const OctokitWithRetry = Octokit.plugin(retry); let octokit: Octokit; const appCfg = getConfig(); diff --git a/docs/documentation/platform/github-org-sync.mdx b/docs/documentation/platform/github-org-sync.mdx new file mode 100644 index 000000000..426c51060 --- /dev/null +++ b/docs/documentation/platform/github-org-sync.mdx @@ -0,0 +1,56 @@ +--- +title: "GitHub Organization Sync" +description: "Learn how to automatically synchronize your GitHub teams with Infisical Groups." +--- + +## Overview + +The GitHub Organization Synchronization feature streamlines user and group management by automatically syncing users belonging to your specified GitHub organization with corresponding groups within Infisical. This integration ensures that users logging in via GitHub are automatically added to or removed from Infisical groups based on their team memberships within your GitHub organization. + +## Configuration + +To enable and configure GitHub Organization Synchronization, follow these steps: + + + + 1. Navigate to **Organization Settings** and select the **Security Tab**. + ![config](../../images/platform/external-syncs/github-org-sync-section.png) + 2. Click the **Configure** button and provide the name of your GitHub Organization. + ![config-modal](../../images/platform/external-syncs/github-org-sync-config-modal.png) + + + Toggle ON GitHub Organization sync to activate sync. + ![toggle-on](../../images/platform/external-syncs/github-org-sync-active.png) + + + Connecting the Infisical OAuth application grants it permission to **read:org** details. This approval is done by selecting your organization during the GitHub OAuth login process. + + 1. Initiate the login process via the GitHub OAuth flow. + ![oauth-flow-start](../../images/platform/external-syncs/github-org-sync-oauth-flow-start.png) + 2. Select the organization you have connected. + 3. Grant access to Infisical oauth application to your configured organization. Infisical shown here is an organization, just for walkthrough. + ![grant-access](../../images/platform/external-syncs/github-org-sync-oauth.png) + + + This action only needs to be done once and authorizes the Infisical OAuth app to read organization details, including team information. + The following users don't need to select organization in GitHub on login anymore. + + + + + +## Working + +Once configured, the GitHub Organization Synchronization feature functions as follows: + +When a user logs in via the GitHub OAuth flow and selects the configured organization, the system will then automatically synchronize the teams they are a part of in GitHub with corresponding groups in Infisical. + +## Troubleshooting + + + If you encounter an error related to this, it indicates that you need to approve the Infisical OAuth application within your GitHub organization. + + You can verify the application's approval status by navigating to **https://github.com/organizations/__your-organization__/settings/oauth_application_policy**. Replace `__your-organization__` with the actual name of your GitHub organization. + + ![check-approval](../../images/platform/external-syncs/github-org-sync-approved-oauth-apps.png) + diff --git a/docs/images/platform/external-syncs/github-org-sync-active.png b/docs/images/platform/external-syncs/github-org-sync-active.png new file mode 100644 index 000000000..bb5ce1ca3 Binary files /dev/null and b/docs/images/platform/external-syncs/github-org-sync-active.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-approved-oauth-apps.png b/docs/images/platform/external-syncs/github-org-sync-approved-oauth-apps.png new file mode 100644 index 000000000..d65d5a43f Binary files /dev/null and b/docs/images/platform/external-syncs/github-org-sync-approved-oauth-apps.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-config-modal.png b/docs/images/platform/external-syncs/github-org-sync-config-modal.png new file mode 100644 index 000000000..b856048e3 Binary files /dev/null and b/docs/images/platform/external-syncs/github-org-sync-config-modal.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-oauth-flow-start.png b/docs/images/platform/external-syncs/github-org-sync-oauth-flow-start.png new file mode 100644 index 000000000..9810e3ddf Binary files /dev/null and b/docs/images/platform/external-syncs/github-org-sync-oauth-flow-start.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-oauth.png b/docs/images/platform/external-syncs/github-org-sync-oauth.png new file mode 100644 index 000000000..68b13c3a7 Binary files /dev/null and b/docs/images/platform/external-syncs/github-org-sync-oauth.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-section.png b/docs/images/platform/external-syncs/github-org-sync-section.png new file mode 100644 index 000000000..dad1fa425 Binary files /dev/null and b/docs/images/platform/external-syncs/github-org-sync-section.png differ diff --git a/docs/mint.json b/docs/mint.json index 47a4c0a76..69470e2b7 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -299,7 +299,8 @@ "documentation/platform/scim/jumpcloud", "documentation/platform/scim/group-mappings" ] - } + }, + "documentation/platform/github-org-sync" ] }, { diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 1c2de52e2..2dbfaacb7 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -35,7 +35,8 @@ export enum OrgPermissionSubjects { AppConnections = "app-connections", Kmip = "kmip", Gateway = "gateway", - SecretShare = "secret-share" + SecretShare = "secret-share", + GithubOrgSync = "github-org-sync" } export enum OrgPermissionAdminConsoleAction { @@ -93,6 +94,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.Scim] + | [OrgPermissionActions, OrgPermissionSubjects.GithubOrgSync] | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups] diff --git a/frontend/src/hooks/api/githubOrgSyncConfig/index.tsx b/frontend/src/hooks/api/githubOrgSyncConfig/index.tsx new file mode 100644 index 000000000..585426469 --- /dev/null +++ b/frontend/src/hooks/api/githubOrgSyncConfig/index.tsx @@ -0,0 +1,6 @@ +export { + useCreateGithubSyncOrgConfig, + useDeleteGithubSyncOrgConfig, + useUpdateGithubSyncOrgConfig +} from "./mutations"; +export { githubOrgSyncConfigQueryKeys } from "./queries"; diff --git a/frontend/src/hooks/api/githubOrgSyncConfig/mutations.tsx b/frontend/src/hooks/api/githubOrgSyncConfig/mutations.tsx new file mode 100644 index 000000000..0cb9b56e8 --- /dev/null +++ b/frontend/src/hooks/api/githubOrgSyncConfig/mutations.tsx @@ -0,0 +1,42 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { githubOrgSyncConfigQueryKeys } from "./queries"; +import { TCreateGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./types"; + +export const useCreateGithubSyncOrgConfig = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (dto: TCreateGithubOrgSyncDTO) => { + return apiRequest.post("/api/v1/github-org-sync-config", dto); + }, + onSuccess: () => { + queryClient.invalidateQueries(githubOrgSyncConfigQueryKeys.get()); + } + }); +}; + +export const useUpdateGithubSyncOrgConfig = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (dto: TUpdateGithubOrgSyncDTO) => { + return apiRequest.patch("/api/v1/github-org-sync-config", dto); + }, + onSuccess: () => { + queryClient.invalidateQueries(githubOrgSyncConfigQueryKeys.get()); + } + }); +}; + +export const useDeleteGithubSyncOrgConfig = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: () => { + return apiRequest.delete("/api/v1/github-org-sync-config"); + }, + onSuccess: () => { + queryClient.invalidateQueries(githubOrgSyncConfigQueryKeys.get()); + } + }); +}; diff --git a/frontend/src/hooks/api/githubOrgSyncConfig/queries.tsx b/frontend/src/hooks/api/githubOrgSyncConfig/queries.tsx new file mode 100644 index 000000000..11bfa97bf --- /dev/null +++ b/frontend/src/hooks/api/githubOrgSyncConfig/queries.tsx @@ -0,0 +1,20 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TGithubOrgSyncConfig } from "./types"; + +export const githubOrgSyncConfigQueryKeys = { + allKey: () => ["github-org-sync-config"], + getKey: () => [...githubOrgSyncConfigQueryKeys.allKey(), "list"], + get: () => + queryOptions({ + queryKey: githubOrgSyncConfigQueryKeys.getKey(), + queryFn: async () => { + const { data } = await apiRequest.get<{ githubOrgSyncConfig: TGithubOrgSyncConfig }>( + "/api/v1/github-org-sync-config" + ); + return data.githubOrgSyncConfig; + } + }) +}; diff --git a/frontend/src/hooks/api/githubOrgSyncConfig/types.ts b/frontend/src/hooks/api/githubOrgSyncConfig/types.ts new file mode 100644 index 000000000..f663c8caf --- /dev/null +++ b/frontend/src/hooks/api/githubOrgSyncConfig/types.ts @@ -0,0 +1,20 @@ +export type TGithubOrgSyncConfig = { + id: string; + orgId: string; + githubOrgAccessToken?: string; + githubOrgName: string; + createdAt: string; + isActive?: boolean; +}; + +export interface TCreateGithubOrgSyncDTO { + githubOrgName: string; + githubOrgAccessToken?: string; + isActive?: boolean; +} + +export interface TUpdateGithubOrgSyncDTO { + githubOrgName?: string; + githubOrgAccessToken?: string; + isActive?: boolean; +} diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 6e8e0ac93..2a80c6174 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -12,6 +12,7 @@ export * from "./certificateTemplates"; export * from "./dynamicSecret"; export * from "./dynamicSecretLease"; export * from "./gateways"; +export * from "./githubOrgSyncConfig"; export * from "./groups"; export * from "./identities"; export * from "./identityProjectAdditionalPrivilege"; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index b6b653ece..ab277ddc8 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -12,6 +12,7 @@ export type SubscriptionPlan = { customAlerts: boolean; customRateLimits: boolean; pitRecovery: boolean; + githubOrgSync: boolean; ipAllowlisting: boolean; rbac: boolean; secretVersioning: boolean; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index d88640cc1..72ebc2987 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -110,6 +110,7 @@ export const formSchema = z.object({ "secret-scanning": generalPermissionSchema, sso: generalPermissionSchema, scim: generalPermissionSchema, + [OrgPermissionSubjects.GithubOrgSync]: generalPermissionSchema, ldap: generalPermissionSchema, billing: generalPermissionSchema, identity: identityPermissionSchema, diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index f077a63a7..7c9cb9850 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -63,6 +63,10 @@ const SIMPLE_PERMISSION_OPTIONS = [ title: "SCIM", formName: "scim" }, + { + title: "GitHub Organization Sync", + formName: OrgPermissionSubjects.GithubOrgSync + }, { title: "External KMS", formName: OrgPermissionSubjects.Kms diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx new file mode 100644 index 000000000..1aa050441 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx @@ -0,0 +1,172 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, DeleteActionModal, FormControl, Input } from "@app/components/v2"; +import { + useCreateGithubSyncOrgConfig, + useDeleteGithubSyncOrgConfig, + useUpdateGithubSyncOrgConfig +} from "@app/hooks/api"; +import { TGithubOrgSyncConfig } from "@app/hooks/api/githubOrgSyncConfig/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z.object({ + githubOrgName: z.string(), + githubOrgAccessToken: z.string().optional() +}); + +export type FormData = z.infer; + +type Props = { + data?: TGithubOrgSyncConfig; + popUp: UsePopUpState<["githubOrgSyncConfig", "deleteGithubOrgSyncConfig"]>; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteGithubOrgSyncConfig"]>, + data?: { + scimTokenId: string; + } + ) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["githubOrgSyncConfig", "deleteGithubOrgSyncConfig"]>, + state?: boolean + ) => void; +}; + +export const GithubOrgSyncConfigModal = ({ + popUp, + handlePopUpOpen, + handlePopUpToggle, + data +}: Props) => { + const isUpdate = Boolean(data); + const { mutateAsync: createGithubSyncOrgConfig } = useCreateGithubSyncOrgConfig(); + const { mutateAsync: updateGithubSyncOrgConfig } = useUpdateGithubSyncOrgConfig(); + const { mutateAsync: deleteGithubSyncOrgConfig } = useDeleteGithubSyncOrgConfig(); + + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + values: data ? { githubOrgName: data.githubOrgName } : undefined + }); + + const onFormSubmit = async ({ githubOrgName, githubOrgAccessToken }: FormData) => { + try { + if (isUpdate) { + await updateGithubSyncOrgConfig({ + githubOrgName, + githubOrgAccessToken + }); + + createNotification({ + text: "Successfully updated GitHub Organization Sync", + type: "success" + }); + } else { + await createGithubSyncOrgConfig({ + githubOrgName, + githubOrgAccessToken, + isActive: false + }); + + createNotification({ + text: "Successfully created GitHub Organization Sync", + type: "success" + }); + } + handlePopUpToggle("githubOrgSyncConfig"); + } catch { + createNotification({ + text: "Failed to setup GitHub Organization Sync", + type: "error" + }); + } + }; + + const onDelete = async () => { + try { + await deleteGithubSyncOrgConfig(); + + handlePopUpToggle("deleteGithubOrgSyncConfig", false); + handlePopUpToggle("githubOrgSyncConfig", false); + createNotification({ + text: "Successfully deleted GitHub Organization Sync", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete GitHub Organization Sync", + type: "error" + }); + } + }; + + return ( + <> +
+ ( + + + + )} + /> + {/* ( + + + + )} + /> */} +
+ + +
+ {isUpdate && ( + + )} +
+ + handlePopUpToggle("deleteGithubOrgSyncConfig", isOpen)} + deleteKey="confirm" + onDeleteApproved={onDelete} + /> + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx index 40d0e7840..bf40c7484 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx @@ -18,6 +18,7 @@ import { LDAPModal } from "./LDAPModal"; import { OIDCModal } from "./OIDCModal"; import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection"; import { OrgGenericAuthSection } from "./OrgGenericAuthSection"; +import { OrgGithubSyncSection } from "./OrgGithubSyncSection"; import { OrgLDAPSection } from "./OrgLDAPSection"; import { OrgOIDCSection } from "./OrgOIDCSection"; import { OrgScimSection } from "./OrgSCIMSection"; @@ -181,6 +182,7 @@ export const OrgAuthTab = withPermission( )} + handlePopUpToggle("upgradePlan", isOpen)} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGithubSyncSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGithubSyncSection.tsx new file mode 100644 index 000000000..c638a280c --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGithubSyncSection.tsx @@ -0,0 +1,114 @@ +import { useQuery } from "@tanstack/react-query"; + +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, Modal, ModalContent, Skeleton, Spinner, Switch } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { githubOrgSyncConfigQueryKeys, useUpdateGithubSyncOrgConfig } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { GithubOrgSyncConfigModal } from "./GithubOrgSyncConfigModal"; + +export const OrgGithubSyncSection = () => { + const { subscription } = useSubscription(); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "upgradePlan", + "githubOrgSyncConfig", + "deleteGithubOrgSyncConfig" + ] as const); + + const githubOrgSyncConfig = useQuery({ + ...githubOrgSyncConfigQueryKeys.get(), + enabled: subscription.githubOrgSync, + retry: false + }); + + const updateGithubSyncOrgConfig = useUpdateGithubSyncOrgConfig(); + + const isPending = subscription.githubOrgSync && githubOrgSyncConfig.isPending; + const data = !isPending && !githubOrgSyncConfig?.isError ? githubOrgSyncConfig?.data : undefined; + + return ( +
+

+ Sync user groups from your GitHub Organization +

+
+
+

GitHub Organization

+
+ + {(isAllowed) => ( + + )} + +
+
+

+ {isPending ? : null} + {data ? data?.githubOrgName : "Not configured"} +

+
+ {data && ( +
+
+

Enable GitHub Sync

+ + {(isAllowed) => ( + + updateGithubSyncOrgConfig.mutate({ + isActive: value + }) + } + isChecked={githubOrgSyncConfig?.data?.isActive ?? false} + isDisabled={!isAllowed} + > + {updateGithubSyncOrgConfig?.isPending && } + + )} + +
+

+ Allow group provisioning/deprovisioning with GitHub +

+
+ )} + { + handlePopUpToggle("githubOrgSyncConfig", isOpen); + }} + > + + + + + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use GitHub Organization Plan if you switch to Infisical's Enterprise plan." + /> +
+ ); +};