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..4eb7eac3d 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 + >; } -} +} \ No newline at end of file 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..3666b3480 --- /dev/null +++ b/backend/src/ee/routes/v1/github-org-sync-router.ts @@ -0,0 +1,127 @@ +import { z } from "zod"; + +import { GithubOrgSyncConfigsSchema } from "@app/db/schemas"; +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 SanitiziedGithubOrgSyncSchema = GithubOrgSyncConfigsSchema.pick({ + isActive: true, + id: true, + createdAt: true, + updatedAt: true, + orgId: true, + githubOrgName: true +}); + +export const registerGithubOrgSyncRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z.object({ + githubOrgName: z.string().trim(), + githubOrgAccessToken: z.string().trim().max(1000).optional(), + isActive: z.boolean().default(false) + }), + response: { + 200: z.object({ + githubOrgSyncConfig: SanitiziedGithubOrgSyncSchema + }) + } + }, + 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: z.string().trim(), + githubOrgAccessToken: z.string().trim().max(1000), + isActive: z.boolean().default(false) + }) + .partial(), + response: { + 200: z.object({ + githubOrgSyncConfig: SanitiziedGithubOrgSyncSchema + }) + } + }, + 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: SanitiziedGithubOrgSyncSchema + }) + } + }, + 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: SanitiziedGithubOrgSyncSchema + }) + } + }, + 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..9ce673a9e --- /dev/null +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -0,0 +1,344 @@ +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 { 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) + 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) + 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: userDetails } = await octoRest.rest.users.getAuthenticated(); + + 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: userDetails.login + } + ) + .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/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..aa2ae6451 --- /dev/null +++ b/frontend/src/hooks/api/githubOrgSyncConfig/types.ts @@ -0,0 +1,20 @@ +export type TGithubOrgSyncConfig = { + id: string; + orgId: string; + githubAccessToken: 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/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx new file mode 100644 index 000000000..65fedbfe8 --- /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("githubOrgSyncConfig", 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." + /> +
+ ); +};