diff --git a/.env.example b/.env.example index be7e0a621..8bcb4039d 100644 --- a/.env.example +++ b/.env.example @@ -36,16 +36,22 @@ CLIENT_ID_HEROKU= CLIENT_ID_VERCEL= CLIENT_ID_NETLIFY= CLIENT_ID_GITHUB= +CLIENT_ID_GITHUB_APP= +CLIENT_SLUG_GITHUB_APP= CLIENT_ID_GITLAB= CLIENT_ID_BITBUCKET= CLIENT_SECRET_HEROKU= CLIENT_SECRET_VERCEL= CLIENT_SECRET_NETLIFY= CLIENT_SECRET_GITHUB= +CLIENT_SECRET_GITHUB_APP= CLIENT_SECRET_GITLAB= CLIENT_SECRET_BITBUCKET= CLIENT_SLUG_VERCEL= +CLIENT_PRIVATE_KEY_GITHUB_APP= +CLIENT_APP_ID_GITHUB_APP= + # Sentry (optional) for monitoring errors SENTRY_DSN= diff --git a/backend/package-lock.json b/backend/package-lock.json index fed409cb6..dbd8c8073 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -28,6 +28,7 @@ "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", "@node-saml/passport-saml": "^4.0.4", + "@octokit/auth-app": "^7.1.1", "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", @@ -5000,24 +5001,73 @@ } }, "node_modules/@octokit/auth-app": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.3.tgz", - "integrity": "sha512-9N7IlBAKEJR3tJgPSubCxIDYGXSdc+2xbkjYpk9nCyqREnH8qEMoMhiEB1WgoA9yTFp91El92XNXAi+AjuKnfw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz", + "integrity": "sha512-kRAd6yelV9OgvlEJE88H0VLlQdZcag9UlLr7dV0YYP37X8PPDvhgiTy66QVhDXdyoT0AleFN2w/qXkPdrSzINg==", "dependencies": { - "@octokit/auth-oauth-app": "^7.0.0", - "@octokit/auth-oauth-user": "^4.0.0", - "@octokit/request": "^8.0.2", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "deprecation": "^2.3.1", + "@octokit/auth-oauth-app": "^8.1.0", + "@octokit/auth-oauth-user": "^5.1.0", + "@octokit/request": "^9.1.1", + "@octokit/request-error": "^6.1.1", + "@octokit/types": "^13.4.1", "lru-cache": "^10.0.0", - "universal-github-app-jwt": "^1.1.2", - "universal-user-agent": "^6.0.0" + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, + "node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/request-error": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, "node_modules/@octokit/auth-app/node_modules/lru-cache": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", @@ -5026,53 +5076,220 @@ "node": "14 || >=16.14" } }, + "node_modules/@octokit/auth-app/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==" + }, "node_modules/@octokit/auth-oauth-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.1.tgz", - "integrity": "sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz", + "integrity": "sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==", "dependencies": { - "@octokit/auth-oauth-device": "^6.0.0", - "@octokit/auth-oauth-user": "^4.0.0", - "@octokit/request": "^8.0.2", - "@octokit/types": "^12.0.0", - "@types/btoa-lite": "^1.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-device": "^7.0.0", + "@octokit/auth-oauth-user": "^5.0.1", + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/auth-oauth-app/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==" + }, "node_modules/@octokit/auth-oauth-device": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.1.tgz", - "integrity": "sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz", + "integrity": "sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==", "dependencies": { - "@octokit/oauth-methods": "^4.0.0", - "@octokit/request": "^8.0.0", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-user": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.1.tgz", - "integrity": "sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==", + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dependencies": { - "@octokit/auth-oauth-device": "^6.0.0", - "@octokit/oauth-methods": "^4.0.0", - "@octokit/request": "^8.0.2", - "@octokit/types": "^12.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/auth-oauth-device/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==" + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz", + "integrity": "sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==", + "dependencies": { + "@octokit/auth-oauth-device": "^7.0.1", + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/auth-oauth-user/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==" + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -5136,28 +5353,82 @@ } }, "node_modules/@octokit/oauth-authorization-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", - "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", "engines": { "node": ">= 18" } }, "node_modules/@octokit/oauth-methods": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.0.1.tgz", - "integrity": "sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.2.tgz", + "integrity": "sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==", "dependencies": { - "@octokit/oauth-authorization-url": "^6.0.2", - "@octokit/request": "^8.0.2", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "btoa-lite": "^1.0.0" + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.1.0", + "@octokit/request-error": "^6.1.0", + "@octokit/types": "^13.0.0" }, "engines": { "node": ">= 18" } }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/oauth-methods/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==" + }, "node_modules/@octokit/openapi-types": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", @@ -5272,13 +5543,13 @@ } }, "node_modules/@octokit/request": { - "version": "8.1.6", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", - "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", "dependencies": { - "@octokit/endpoint": "^9.0.0", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" }, "engines": { @@ -5286,11 +5557,11 @@ } }, "node_modules/@octokit/request-error": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", - "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "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==", "dependencies": { - "@octokit/types": "^12.0.0", + "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" }, @@ -5298,6 +5569,32 @@ "node": ">= 18" } }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, "node_modules/@octokit/rest": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz", @@ -14184,6 +14481,154 @@ "@octokit/core": ">=5" } }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-app": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.2.tgz", + "integrity": "sha512-fWjIOpxnL8/YFY3kqquciFQ4o99aCqHw5kMFoGPYbz/h5HNZ11dJlV9zag5wS2nt0X1wJ5cs9BUo+CsAPfW4jQ==", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "^10.0.0", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/octokit-auth-probot/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/octokit-auth-probot/node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -18176,13 +18621,9 @@ } }, "node_modules/universal-github-app-jwt": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz", - "integrity": "sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==", - "dependencies": { - "@types/jsonwebtoken": "^9.0.0", - "jsonwebtoken": "^9.0.2" - } + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", + "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==" }, "node_modules/universal-user-agent": { "version": "6.0.1", diff --git a/backend/package.json b/backend/package.json index 13eb931be..97e951242 100644 --- a/backend/package.json +++ b/backend/package.json @@ -132,6 +132,7 @@ "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", "@node-saml/passport-saml": "^4.0.4", + "@octokit/auth-app": "^7.1.1", "@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 65da73072..21c44a3b5 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -39,6 +39,7 @@ import { TCertificateServiceFactory } from "@app/services/certificate/certificat import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; +import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; @@ -185,6 +186,7 @@ declare module "fastify" { workflowIntegration: TWorkflowIntegrationServiceFactory; cmek: TCmekServiceFactory; migration: TExternalMigrationServiceFactory; + externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory; }; // 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 624915276..fb78bce4d 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -336,6 +336,11 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; +import { + TExternalGroupOrgRoleMappings, + TExternalGroupOrgRoleMappingsInsert, + TExternalGroupOrgRoleMappingsUpdate +} from "@app/db/schemas/external-group-org-role-mappings"; import { TSecretV2TagJunction, TSecretV2TagJunctionInsert, @@ -808,5 +813,10 @@ declare module "knex/types/tables" { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate >; + [TableName.ExternalGroupOrgRoleMapping]: KnexOriginal.CompositeTableType< + TExternalGroupOrgRoleMappings, + TExternalGroupOrgRoleMappingsInsert, + TExternalGroupOrgRoleMappingsUpdate + >; } } diff --git a/backend/src/db/migrations/20241015145450_external-group-org-role-mapping.ts b/backend/src/db/migrations/20241015145450_external-group-org-role-mapping.ts new file mode 100644 index 000000000..728d49c25 --- /dev/null +++ b/backend/src/db/migrations/20241015145450_external-group-org-role-mapping.ts @@ -0,0 +1,32 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils"; + +export async function up(knex: Knex): Promise { + // add external group to org role mapping table + if (!(await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping))) { + await knex.schema.createTable(TableName.ExternalGroupOrgRoleMapping, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("groupName").notNullable(); + t.index("groupName"); + t.string("role").notNullable(); + t.uuid("roleId"); + t.foreign("roleId").references("id").inTable(TableName.OrgRoles); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + t.unique(["orgId", "groupName"]); + }); + + await createOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping)) { + await dropOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping); + + await knex.schema.dropTable(TableName.ExternalGroupOrgRoleMapping); + } +} diff --git a/backend/src/db/schemas/external-group-org-role-mappings.ts b/backend/src/db/schemas/external-group-org-role-mappings.ts new file mode 100644 index 000000000..f7e6eab25 --- /dev/null +++ b/backend/src/db/schemas/external-group-org-role-mappings.ts @@ -0,0 +1,27 @@ +// 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 { TImmutableDBKeys } from "./models"; + +export const ExternalGroupOrgRoleMappingsSchema = z.object({ + id: z.string().uuid(), + groupName: z.string(), + role: z.string(), + roleId: z.string().uuid().nullable().optional(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TExternalGroupOrgRoleMappings = z.infer; +export type TExternalGroupOrgRoleMappingsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TExternalGroupOrgRoleMappingsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 08f3e79ce..7b48bb6fc 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -17,6 +17,7 @@ export enum TableName { Groups = "groups", GroupProjectMembership = "group_project_memberships", GroupProjectMembershipRole = "group_project_membership_roles", + ExternalGroupOrgRoleMapping = "external_group_org_role_mappings", UserGroupMembership = "user_group_membership", UserAliases = "user_aliases", UserEncryptionKey = "user_encryption_keys", diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index 427c77fa7..cd5f2f9f3 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -20,7 +20,7 @@ const ScimUserSchema = z.object({ z.object({ primary: z.boolean(), value: z.string().email(), - type: z.string().trim() + type: z.string().trim().default("work") }) ) .optional(), @@ -210,8 +210,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { .array( z.object({ primary: z.boolean(), - value: z.string().email(), - type: z.string().trim() + value: z.string().email() }) ) .optional(), @@ -281,8 +280,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { .array( z.object({ primary: z.boolean(), - value: z.string().email(), - type: z.string().trim() + value: z.string().email() }) ) .optional(), @@ -301,7 +299,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { z.object({ primary: z.boolean(), value: z.string().email(), - type: z.string().trim() + type: z.string().trim().default("work") }) ), displayName: z.string().trim(), diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 83bc98009..2744b34c2 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -190,7 +190,9 @@ export enum EventType { DELETE_CMEK = "delete-cmek", GET_CMEKS = "get-cmeks", CMEK_ENCRYPT = "cmek-encrypt", - CMEK_DECRYPT = "cmek-decrypt" + CMEK_DECRYPT = "cmek-decrypt", + UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping", + GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping" } interface UserActorMetadata { @@ -1604,6 +1606,18 @@ interface CmekDecryptEvent { }; } +interface GetExternalGroupOrgRoleMappingsEvent { + type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS; + metadata?: Record; // not needed, based off orgId +} + +interface UpdateExternalGroupOrgRoleMappingsEvent { + type: EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS; + metadata: { + mappings: { groupName: string; roleSlug: string }[]; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -1750,4 +1764,6 @@ export type Event = | DeleteCmekEvent | GetCmeksEvent | CmekEncryptEvent - | CmekDecryptEvent; + | CmekDecryptEvent + | GetExternalGroupOrgRoleMappingsEvent + | UpdateExternalGroupOrgRoleMappingsEvent; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 8b94d5b93..9165408fa 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify"; import jwt from "jsonwebtoken"; import { scimPatch } from "scim-patch"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas"; +import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -13,6 +13,7 @@ import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } f import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TOrgPermission } from "@app/lib/types"; import { AuthTokenType } from "@app/services/auth/auth-type"; +import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { deleteOrgMembershipFn } from "@app/services/org/org-fns"; @@ -71,7 +72,10 @@ type TScimServiceFactoryDep = { | "transaction" | "updateMembershipById" >; - orgMembershipDAL: Pick; + orgMembershipDAL: Pick< + TOrgMembershipDALFactory, + "find" | "findOne" | "create" | "updateById" | "findById" | "update" + >; projectDAL: Pick; projectMembershipDAL: Pick; groupDAL: Pick< @@ -102,6 +106,7 @@ type TScimServiceFactoryDep = { permissionService: Pick; smtpService: Pick; projectUserAdditionalPrivilegeDAL: Pick; + externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; }; export type TScimServiceFactory = ReturnType; @@ -122,7 +127,8 @@ export const scimServiceFactory = ({ projectBotDAL, permissionService, projectUserAdditionalPrivilegeDAL, - smtpService + smtpService, + externalGroupOrgRoleMappingDAL }: TScimServiceFactoryDep) => { const createScimToken = async ({ actor, @@ -692,6 +698,43 @@ export const scimServiceFactory = ({ }); }; + const $syncNewMembersRoles = async (group: TGroups, members: TScimGroup["members"]) => { + // this function handles configuring newly provisioned users org membership if an external group mapping exists + + if (!members.length) return; + + const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ + orgId: group.orgId, + groupName: group.name + }); + + // no mapping, user will have default org membership + if (!externalGroupMapping) return; + + // only get org memberships that are new (invites) + const newOrgMemberships = await orgMembershipDAL.find({ + status: "invited", + $in: { + id: members.map((member) => member.value) + } + }); + + if (!newOrgMemberships.length) return; + + // set new membership roles to group mapping value + await orgMembershipDAL.update( + { + $in: { + id: newOrgMemberships.map((membership) => membership.id) + } + }, + { + role: externalGroupMapping.role, + roleId: externalGroupMapping.roleId + } + ); + }; + const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) @@ -745,6 +788,8 @@ export const scimServiceFactory = ({ tx }); + await $syncNewMembersRoles(group, members); + return { group, newMembers }; } @@ -820,22 +865,41 @@ export const scimServiceFactory = ({ orgId: string, { displayName, members = [] }: { displayName: string; members: { value: string }[] } ) => { - const updatedGroup = await groupDAL.transaction(async (tx) => { - const [group] = await groupDAL.update( - { - id: groupId, - orgId - }, - { - name: displayName - } - ); + let group = await groupDAL.findOne({ + id: groupId, + orgId + }); - if (!group) { - throw new ScimRequestError({ - detail: "Group Not Found", - status: 404 - }); + if (!group) { + throw new ScimRequestError({ + detail: "Group Not Found", + status: 404 + }); + } + + const updatedGroup = await groupDAL.transaction(async (tx) => { + if (group.name !== displayName) { + await externalGroupOrgRoleMappingDAL.update( + { + groupName: group.name, + orgId + }, + { + groupName: displayName + } + ); + + const [modifiedGroup] = await groupDAL.update( + { + id: groupId, + orgId + }, + { + name: displayName + } + ); + + group = modifiedGroup; } const orgMemberships = members.length @@ -892,6 +956,8 @@ export const scimServiceFactory = ({ return group; }); + await $syncNewMembersRoles(group, members); + return updatedGroup; }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index b047de755..2b7a3a733 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -117,9 +117,16 @@ const envSchema = z // gcp secret manager CLIENT_ID_GCP_SECRET_MANAGER: zpStr(z.string().optional()), CLIENT_SECRET_GCP_SECRET_MANAGER: zpStr(z.string().optional()), - // github + // github oauth CLIENT_ID_GITHUB: zpStr(z.string().optional()), CLIENT_SECRET_GITHUB: zpStr(z.string().optional()), + // github app + CLIENT_ID_GITHUB_APP: zpStr(z.string().optional()), + CLIENT_SECRET_GITHUB_APP: zpStr(z.string().optional()), + CLIENT_PRIVATE_KEY_GITHUB_APP: zpStr(z.string().optional()), + CLIENT_APP_ID_GITHUB_APP: z.coerce.number().optional(), + CLIENT_SLUG_GITHUB_APP: zpStr(z.string().optional()), + // azure CLIENT_ID_AZURE: zpStr(z.string().optional()), CLIENT_SECRET_AZURE: zpStr(z.string().optional()), diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2b84e2881..326b283f5 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -97,6 +97,8 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; +import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; +import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; @@ -336,6 +338,8 @@ export const registerRoutes = async ( const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const workflowIntegrationDAL = workflowIntegrationDALFactory(db); + const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db); + const permissionService = permissionServiceFactory({ permissionDAL, orgRoleDAL, @@ -442,7 +446,8 @@ export const registerRoutes = async ( projectKeyDAL, projectBotDAL, permissionService, - smtpService + smtpService, + externalGroupOrgRoleMappingDAL }); const ldapService = ldapConfigServiceFactory({ @@ -537,7 +542,12 @@ export const registerRoutes = async ( orgService, licenseService }); - const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL, orgDAL }); + const orgRoleService = orgRoleServiceFactory({ + permissionService, + orgRoleDAL, + orgDAL, + externalGroupOrgRoleMappingDAL + }); const superAdminService = superAdminServiceFactory({ userDAL, authService: loginService, @@ -1231,6 +1241,13 @@ export const registerRoutes = async ( permissionService }); + const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({ + permissionService, + licenseService, + orgRoleDAL, + externalGroupOrgRoleMappingDAL + }); + await superAdminService.initServerCfg(); // // setup the communication with license key server @@ -1316,7 +1333,8 @@ export const registerRoutes = async ( orgAdmin: orgAdminService, slack: slackService, workflowIntegration: workflowIntegrationService, - migration: migrationService + migration: migrationService, + externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 9a3480288..e6da3ad73 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -109,7 +109,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { firstName: true, lastName: true, email: true, - id: true + id: true, + superAdmin: true }).array() }) } diff --git a/backend/src/server/routes/v1/external-group-org-role-mapping-router.ts b/backend/src/server/routes/v1/external-group-org-role-mapping-router.ts new file mode 100644 index 000000000..032deda7d --- /dev/null +++ b/backend/src/server/routes/v1/external-group-org-role-mapping-router.ts @@ -0,0 +1,83 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +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"; + +export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZodProvider) => { + // get mappings for current org + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: ExternalGroupOrgRoleMappingsSchema.array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const mappings = server.services.externalGroupOrgRoleMapping.listExternalGroupOrgRoleMappings(req.permission); + + await server.services.auditLog.createAuditLog({ + orgId: req.permission.orgId, + ...req.auditLogInfo, + event: { + type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS + } + }); + + return mappings; + } + }); + + // update mappings for current org + server.route({ + method: "PUT", // using put since this endpoint creates, updates and deletes mappings + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + mappings: z + .object({ + groupName: z.string().trim().min(1), + roleSlug: z + .string() + .min(1) + .toLowerCase() + .refine((v) => slugify(v) === v, { + message: "Role must be a valid slug" + }) + }) + .array() + }), + response: { + 200: ExternalGroupOrgRoleMappingsSchema.array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { body, permission } = req; + + const mappings = server.services.externalGroupOrgRoleMapping.updateExternalGroupOrgRoleMappings(body, permission); + + await server.services.auditLog.createAuditLog({ + orgId: permission.orgId, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS, + metadata: body + } + }); + + return mappings; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 55b323656..f9edfc18c 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -7,6 +7,7 @@ import { registerProjectBotRouter } from "./bot-router"; import { registerCaRouter } from "./certificate-authority-router"; import { registerCertRouter } from "./certificate-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router"; +import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; @@ -106,4 +107,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" }); await server.register(registerCmekRouter, { prefix: "/kms" }); + await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" }); }; diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 4baa39f76..1d2959f5b 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -189,6 +189,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) workspaceId: z.string().trim(), code: z.string().trim(), integration: z.string().trim(), + installationId: z.string().trim().optional(), url: z.string().trim().url().optional() }), response: { @@ -452,6 +453,40 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "POST", + url: "/:integrationAuthId/duplicate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + body: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + integrationAuth: integrationAuthPubSchema + }) + } + }, + handler: async (req) => { + const integrationAuth = await server.services.integrationAuth.duplicateIntegrationAuth({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + id: req.params.integrationAuthId, + projectId: req.body.projectId + }); + + return { integrationAuth }; + } + }); + server.route({ method: "GET", url: "/:integrationAuthId/github/envs", diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index f6d5cacef..30d032e13 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -17,6 +17,8 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { integrationAuthPubSchema } from "../sanitizedSchemas"; + export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", @@ -68,6 +70,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:organizationId/integration-authorizations", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + organizationId: z.string().trim() + }), + response: { + 200: z.object({ + authorizations: integrationAuthPubSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const authorizations = await server.services.integrationAuth.listOrgIntegrationAuth({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId + }); + + return { authorizations }; + } + }); + server.route({ method: "GET", url: "/audit-logs", @@ -180,7 +211,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { email: true, firstName: true, lastName: true, - id: true + id: true, + superAdmin: true }).merge(z.object({ publicKey: z.string().nullable() })) }) ) diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-dal.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-dal.ts new file mode 100644 index 000000000..6f8f5973c --- /dev/null +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-dal.ts @@ -0,0 +1,46 @@ +import { Tables } from "knex/types/tables"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TExternalGroupOrgRoleMappings } from "@app/db/schemas/external-group-org-role-mappings"; +import { ormify } from "@app/lib/knex"; + +export type TExternalGroupOrgRoleMappingDALFactory = ReturnType; + +export const externalGroupOrgRoleMappingDALFactory = (db: TDbClient) => { + const externalGroupOrgRoleMappingOrm = ormify(db, TableName.ExternalGroupOrgRoleMapping); + + const updateExternalGroupOrgRoleMappingForOrg = async ( + orgId: string, + newMappings: readonly Tables[TableName.ExternalGroupOrgRoleMapping]["insert"][] + ) => { + const currentMappings = await externalGroupOrgRoleMappingOrm.find({ orgId }); + + const newMap = new Map(newMappings.map((mapping) => [mapping.groupName, mapping])); + const currentMap = new Map(currentMappings.map((mapping) => [mapping.groupName, mapping])); + + const mappingsToDelete = currentMappings.filter((mapping) => !newMap.has(mapping.groupName)); + const mappingsToUpdate = currentMappings + .filter((mapping) => newMap.has(mapping.groupName)) + .map((mapping) => ({ id: mapping.id, ...newMap.get(mapping.groupName) })); + const mappingsToInsert = newMappings.filter((mapping) => !currentMap.has(mapping.groupName)); + + const mappings = await externalGroupOrgRoleMappingOrm.transaction(async (tx) => { + await externalGroupOrgRoleMappingOrm.delete({ $in: { id: mappingsToDelete.map((mapping) => mapping.id) } }, tx); + + const updatedMappings: TExternalGroupOrgRoleMappings[] = []; + for await (const { id, ...mappingData } of mappingsToUpdate) { + const updatedMapping = await externalGroupOrgRoleMappingOrm.update({ id }, mappingData, tx); + updatedMappings.push(updatedMapping[0]); + } + + const insertedMappings = await externalGroupOrgRoleMappingOrm.insertMany(mappingsToInsert, tx); + + return [...updatedMappings, ...insertedMappings]; + }); + + return mappings; + }; + + return { ...externalGroupOrgRoleMappingOrm, updateExternalGroupOrgRoleMappingForOrg }; +}; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts new file mode 100644 index 000000000..fe6724251 --- /dev/null +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts @@ -0,0 +1,67 @@ +import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; + +import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types"; + +export const constructGroupOrgMembershipRoleMappings = async ({ + mappingsDTO, + orgId, + orgRoleDAL, + licenseService +}: { + mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[]; + orgRoleDAL: TOrgRoleDALFactory; + licenseService: TLicenseServiceFactory; + orgId: string; +}) => { + const plan = await licenseService.getPlan(orgId); + + // prevent setting custom values if not in plan + if (mappingsDTO.some((map) => isCustomOrgRole(map.roleSlug)) && !plan?.rbac) + throw new BadRequestError({ + message: + "Failed to set group organization role mapping due to plan RBAC restriction. Upgrade plan to set custom role mapping." + }); + + const customRoleSlugs = mappingsDTO + .filter((mapping) => isCustomOrgRole(mapping.roleSlug)) + .map((mapping) => mapping.roleSlug); + + let customRolesMap: Map = new Map(); + if (customRoleSlugs.length > 0) { + const customRoles = await orgRoleDAL.find({ + $in: { + slug: customRoleSlugs + } + }); + + customRolesMap = new Map(customRoles.map((role) => [role.slug, role])); + } + + const mappings = mappingsDTO.map(({ roleSlug, groupName }) => { + if (isCustomOrgRole(roleSlug)) { + const customRole = customRolesMap.get(roleSlug); + + if (!customRole) throw new NotFoundError({ message: `Custom role ${roleSlug} not found.` }); + + return { + groupName, + role: OrgMembershipRole.Custom, + roleId: customRole.id, + orgId + }; + } + + return { + groupName, + role: roleSlug, + roleId: null, // need to set explicitly null for updates + orgId + }; + }); + + return mappings; +}; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts new file mode 100644 index 000000000..2d116eb38 --- /dev/null +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts @@ -0,0 +1,78 @@ +import { ForbiddenError } from "@casl/ability"; +import { FastifyRequest } from "fastify"; + +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns"; +import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types"; +import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; + +import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal"; + +type TExternalGroupOrgRoleMappingServiceFactoryDep = { + externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; + permissionService: TPermissionServiceFactory; + licenseService: TLicenseServiceFactory; + orgRoleDAL: TOrgRoleDALFactory; +}; + +export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType; + +export const externalGroupOrgRoleMappingServiceFactory = ({ + externalGroupOrgRoleMappingDAL, + licenseService, + permissionService, + orgRoleDAL +}: TExternalGroupOrgRoleMappingServiceFactoryDep) => { + const listExternalGroupOrgRoleMappings = async (actor: FastifyRequest["permission"]) => { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + // TODO: will need to change if we add support for ldap, oidc, etc. + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); + + const mappings = await externalGroupOrgRoleMappingDAL.find({ + orgId: actor.orgId + }); + + return mappings; + }; + + const updateExternalGroupOrgRoleMappings = async ( + dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO, + actor: FastifyRequest["permission"] + ) => { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + // TODO: will need to change if we add support for ldap, oidc, etc. + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); + + const mappings = await constructGroupOrgMembershipRoleMappings({ + mappingsDTO: dto.mappings, + orgRoleDAL, + licenseService, + orgId: actor.orgId + }); + + const data = await externalGroupOrgRoleMappingDAL.updateExternalGroupOrgRoleMappingForOrg(actor.orgId, mappings); + + return data; + }; + + return { + updateExternalGroupOrgRoleMappings, + listExternalGroupOrgRoleMappings + }; +}; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-types.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-types.ts new file mode 100644 index 000000000..2b2ccc10c --- /dev/null +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-types.ts @@ -0,0 +1,8 @@ +export type TExternalGroupOrgMembershipRoleMappingDTO = { + groupName: string; + roleSlug: string; +}; + +export type TSyncExternalGroupOrgMembershipRoleMappingsDTO = { + mappings: TExternalGroupOrgMembershipRoleMappingDTO[]; +}; diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index e61aa4a54..44ac825c7 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -1,9 +1,13 @@ /* eslint-disable no-await-in-loop */ +import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; +import { TIntegrationAuths } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { NotFoundError } from "@app/lib/errors"; +import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { Integrations, IntegrationUrls } from "./integration-list"; // akhilmhdh: check this part later. Copied from old base @@ -230,7 +234,13 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { /** * Return list of repositories for Github integration */ -const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { +const getAppsGithub = async ({ + accessToken, + authMetadata +}: { + accessToken: string; + authMetadata?: TIntegrationAuthMetadata; +}) => { interface GitHubApp { id: string; name: string; @@ -242,6 +252,29 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { }; } + if (authMetadata?.installationId) { + const appCfg = getConfig(); + const octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: appCfg.CLIENT_APP_ID_GITHUB_APP, + privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP, + installationId: authMetadata.installationId + } + }); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const repos = await octokit.paginate("GET /installation/repositories", { + per_page: 100 + }); + + return repos.map((a) => ({ + appId: String(a.id), + name: a.name, + owner: a.owner.login + })); + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const repos = (await new Octokit({ auth: accessToken @@ -1056,6 +1089,7 @@ const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: strin export const getApps = async ({ integration, + integrationAuth, accessToken, accessId, teamId, @@ -1066,6 +1100,7 @@ export const getApps = async ({ integration: string; accessToken: string; accessId?: string; + integrationAuth: TIntegrationAuths; teamId?: string | null; azureDevOpsOrgName?: string | null; workspaceSlug?: string; @@ -1099,7 +1134,8 @@ export const getApps = async ({ case Integrations.GITHUB: return getAppsGithub({ - accessToken + accessToken, + authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}) }); case Integrations.GITLAB: diff --git a/backend/src/services/integration-auth/integration-auth-dal.ts b/backend/src/services/integration-auth/integration-auth-dal.ts index d32cd1579..7a56afcbb 100644 --- a/backend/src/services/integration-auth/integration-auth-dal.ts +++ b/backend/src/services/integration-auth/integration-auth-dal.ts @@ -3,7 +3,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TIntegrationAuthDALFactory = ReturnType; @@ -28,8 +28,23 @@ export const integrationAuthDALFactory = (db: TDbClient) => { } }; + const getByOrg = async (orgId: string, tx?: Knex) => { + try { + const integrationAuths = await (tx || db)(TableName.IntegrationAuth) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.IntegrationAuth}.projectId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) + .where(`${TableName.Organization}.id`, "=", orgId) + .select(selectAllTableCols(TableName.IntegrationAuth)); + + return integrationAuths; + } catch (error) { + throw new DatabaseError({ error, name: "get by org" }); + } + }; + return { ...integrationAuthOrm, - bulkUpdate + bulkUpdate, + getByOrg }; }; diff --git a/backend/src/services/integration-auth/integration-auth-schema.ts b/backend/src/services/integration-auth/integration-auth-schema.ts new file mode 100644 index 000000000..94a68cc72 --- /dev/null +++ b/backend/src/services/integration-auth/integration-auth-schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const IntegrationAuthMetadataSchema = z.object({ + installationId: z.string().optional() +}); + +export type TIntegrationAuthMetadata = z.infer; diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index c3e0dfe06..728e417cf 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -1,14 +1,16 @@ import { ForbiddenError } from "@casl/ability"; +import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; import AWS from "aws-sdk"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { TProjectPermission } from "@app/lib/types"; +import { TGenericPermission, TProjectPermission } from "@app/lib/types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -16,11 +18,13 @@ import { KmsDataKey } from "../kms/kms-types"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { getApps } from "./integration-app-list"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; +import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { TBitbucketWorkspace, TChecklyGroups, TDeleteIntegrationAuthByIdDTO, TDeleteIntegrationAuthsDTO, + TDuplicateGithubIntegrationAuthDTO, TGetIntegrationAuthDTO, TGetIntegrationAuthTeamCityBuildConfigDTO, THerokuPipelineCoupling, @@ -86,6 +90,24 @@ export const integrationAuthServiceFactory = ({ return authorizations; }; + const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => { + const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string); + + return Promise.all( + authorizations.filter(async (auth) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + auth.projectId, + actorAuthMethod, + actorOrgId + ); + + return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + }) + ); + }; + const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" }); @@ -109,7 +131,8 @@ export const integrationAuthServiceFactory = ({ actorAuthMethod, integration, url, - code + code, + installationId }: TOauthExchangeDTO) => { if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); @@ -123,7 +146,7 @@ export const integrationAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); - const tokenExchange = await exchangeCode({ integration, code, url }); + const tokenExchange = await exchangeCode({ integration, code, url, installationId }); const updateDoc: TIntegrationAuthsInsert = { projectId, integration, @@ -141,6 +164,16 @@ export const integrationAuthServiceFactory = ({ updateDoc.metadata = { authMethod: "oauth2" }; + } else if (integration === Integrations.GITHUB && installationId) { + updateDoc.metadata = { + installationId, + installationName: tokenExchange.installationName, + authMethod: "app" + }; + } + + if (installationId && integration === Integrations.GITHUB) { + return integrationAuthDAL.create(updateDoc); } const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); @@ -176,12 +209,23 @@ export const integrationAuthServiceFactory = ({ updateDoc.accessCiphertext = accessEncToken.ciphertext; } } + return integrationAuthDAL.transaction(async (tx) => { - const doc = await integrationAuthDAL.findOne({ projectId, integration }, tx); - if (!doc) { + const integrationAuths = await integrationAuthDAL.find({ projectId, integration }, { tx }); + let existingIntegrationAuth: TIntegrationAuths | undefined; + + // we need to ensure that the integration auth that we use for Github is actually Oauth + if (integration === Integrations.GITHUB) { + existingIntegrationAuth = integrationAuths.find((integAuth) => !integAuth.metadata); + } else { + [existingIntegrationAuth] = integrationAuths; + } + + if (!existingIntegrationAuth) { return integrationAuthDAL.create(updateDoc, tx); } - return integrationAuthDAL.updateById(doc.id, updateDoc, tx); + + return integrationAuthDAL.updateById(existingIntegrationAuth.id, updateDoc, tx); }); }; @@ -334,6 +378,13 @@ export const integrationAuthServiceFactory = ({ ) { return { accessToken: "", accessId: "" }; } + if ( + integrationAuth.integration === Integrations.GITHUB && + IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}).installationId + ) { + return { accessToken: "", accessId: "" }; + } + if (shouldUseSecretV2Bridge) { const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ @@ -460,6 +511,7 @@ export const integrationAuthServiceFactory = ({ const { accessToken, accessId } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); const apps = await getApps({ integration: integrationAuth.integration, + integrationAuth, accessToken, accessId, teamId, @@ -575,6 +627,7 @@ export const integrationAuthServiceFactory = ({ }; const getGithubOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthGithubOrgsDTO) => { + const appCfg = getConfig(); const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" }); @@ -587,9 +640,44 @@ export const integrationAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); - const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); - const octokit = new Octokit({ + let octokit: Octokit; + const { installationId } = (integrationAuth.metadata as TIntegrationAuthMetadata) || {}; + if (installationId) { + octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: appCfg.CLIENT_APP_ID_GITHUB_APP, + privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP, + installationId + } + }); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const repos = await octokit.paginate("GET /installation/repositories", { + per_page: 100 + }); + + const orgSet: Set = new Set(); + + return repos + .filter((repo) => repo.owner.type === "Organization") + .map((repo) => ({ + name: repo.owner.login, + orgId: String(repo.owner.id) + })) + .filter((org) => { + const isOrgProcessed = orgSet.has(org.orgId); + if (!isOrgProcessed) { + orgSet.add(org.orgId); + } + + return !isOrgProcessed; + }); + } + + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + octokit = new Octokit({ auth: accessToken }); @@ -598,7 +686,9 @@ export const integrationAuthServiceFactory = ({ "X-GitHub-Api-Version": "2022-11-28" } }); - if (!data) return []; + if (!data) { + return []; + } return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) })); }; @@ -626,9 +716,24 @@ export const integrationAuthServiceFactory = ({ const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); - const octokit = new Octokit({ - auth: accessToken - }); + let octokit: Octokit; + const appCfg = getConfig(); + + const authMetadata = IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}); + if (authMetadata.installationId) { + octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: appCfg.CLIENT_APP_ID_GITHUB_APP, + privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP, + installationId: authMetadata.installationId + } + }); + } else { + octokit = new Octokit({ + auth: accessToken + }); + } const { data: { environments } @@ -1315,8 +1420,58 @@ export const integrationAuthServiceFactory = ({ return delIntegrationAuth; }; + // At the moment, we only use this for Github App integration as it's a special case + const duplicateIntegrationAuth = async ({ + id, + actorId, + actor, + actorAuthMethod, + actorOrgId, + projectId + }: TDuplicateGithubIntegrationAuthDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) { + throw new NotFoundError({ message: "Failed to find integration" }); + } + + const { permission: sourcePermission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(sourcePermission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); + + const { permission: targetPermission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(targetPermission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); + + const newIntegrationAuth: Omit & { id?: string } = { + ...integrationAuth, + id: undefined, + projectId + }; + + return integrationAuthDAL.create(newIntegrationAuth); + }; + return { listIntegrationAuthByProjectId, + listOrgIntegrationAuth, getIntegrationOptions, getIntegrationAuth, oauthExchange, @@ -1343,6 +1498,7 @@ export const integrationAuthServiceFactory = ({ getNorthFlankSecretGroups, getTeamcityBuildConfigs, getBitbucketWorkspaces, - getIntegrationAccessToken + getIntegrationAccessToken, + duplicateIntegrationAuth }; }; diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index af390297a..eb8b8044d 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -9,6 +9,7 @@ export type TOauthExchangeDTO = { integration: string; code: string; url?: string; + installationId?: string; } & TProjectPermission; export type TSaveIntegrationAccessTokenDTO = { @@ -107,6 +108,10 @@ export type TDeleteIntegrationAuthByIdDTO = { id: string; } & Omit; +export type TDuplicateGithubIntegrationAuthDTO = { + id: string; +} & TProjectPermission; + export type TGetIntegrationAuthTeamCityBuildConfigDTO = { id: string; appId: string; diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index bd9619ad4..7cf77cb26 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -1,7 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { createAppAuth } from "@octokit/auth-app"; import { retry } from "@octokit/plugin-retry"; import { Octokit } from "@octokit/rest"; import { TIntegrationAuths, TIntegrations } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -15,6 +18,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; +import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { TIntegrationAuthServiceFactory } from "./integration-auth-service"; import { Integrations } from "./integration-list"; @@ -154,10 +158,12 @@ const getIntegrationSecretsV1 = async ( export const deleteGithubSecrets = async ({ integration, + authMetadata, secrets, accessToken }: { integration: Omit; + authMetadata: TIntegrationAuthMetadata; secrets: Record; accessToken: string; }) => { @@ -170,9 +176,23 @@ export const deleteGithubSecrets = async ({ } const OctokitWithRetry = Octokit.plugin(retry); - const octokit = new OctokitWithRetry({ - auth: accessToken - }); + let octokit: Octokit; + const appCfg = getConfig(); + + if (authMetadata.installationId) { + octokit = new OctokitWithRetry({ + authStrategy: createAppAuth, + auth: { + appId: appCfg.CLIENT_APP_ID_GITHUB_APP, + privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP, + installationId: authMetadata.installationId + } + }); + } else { + octokit = new OctokitWithRetry({ + auth: accessToken + }); + } enum GithubScope { Repo = "github-repo", @@ -192,6 +212,7 @@ export const deleteGithubSecrets = async ({ break; } case GithubScope.Env: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment encryptedGithubSecrets = ( await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", { repository_id: Number(integration.appId), @@ -346,6 +367,7 @@ export const deleteIntegrationSecrets = async ({ case Integrations.GITHUB: { await deleteGithubSecrets({ integration, + authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}), accessToken, secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets }); diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index ef6d0160e..af9d358ed 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -96,7 +96,9 @@ export enum IntegrationUrls { GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com", - GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform" + GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform", + + GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations" } export const getIntegrationOptions = async () => { @@ -138,6 +140,7 @@ export const getIntegrationOptions = async () => { isAvailable: true, type: "oauth", clientId: appCfg.CLIENT_ID_GITHUB, + clientSlug: appCfg.CLIENT_SLUG_GITHUB_APP, docsLink: "" }, { diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index edd37dc74..7913b4029 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -19,6 +19,7 @@ import { UpdateSecretCommand } from "@aws-sdk/client-secrets-manager"; import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; import AWS, { AWSError } from "aws-sdk"; import { AxiosError } from "axios"; @@ -36,6 +37,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/ import { TIntegrationDALFactory } from "../integration/integration-dal"; import { IntegrationMetadataSchema } from "../integration/integration-schema"; +import { IntegrationAuthMetadataSchema } from "./integration-auth-schema"; import { TIntegrationsWithEnvironment } from "./integration-auth-types"; import { IntegrationInitialSyncBehavior, @@ -1542,11 +1544,13 @@ const syncSecretsNetlify = async ({ */ const syncSecretsGitHub = async ({ integration, + integrationAuth, secrets, accessToken, appendices }: { integration: TIntegrations; + integrationAuth: TIntegrationAuths; secrets: Record; accessToken: string; appendices?: { prefix: string; suffix: string }; @@ -1568,9 +1572,24 @@ const syncSecretsGitHub = async ({ selected_repositories_url?: string | undefined; } - const octokit = new Octokit({ - auth: accessToken - }); + const authMetadata = IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}); + let octokit: Octokit; + const appCfg = getConfig(); + + if (authMetadata.installationId) { + octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: appCfg.CLIENT_APP_ID_GITHUB_APP, + privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP, + installationId: authMetadata.installationId + } + }); + } else { + octokit = new Octokit({ + auth: accessToken + }); + } enum GithubScope { Repo = "github-repo", @@ -4069,6 +4088,7 @@ export const syncIntegrationSecrets = async ({ case Integrations.GITHUB: await syncSecretsGitHub({ integration, + integrationAuth, secrets, accessToken, appendices diff --git a/backend/src/services/integration-auth/integration-token.ts b/backend/src/services/integration-auth/integration-token.ts index ba26a3aaa..9b4e5c20f 100644 --- a/backend/src/services/integration-auth/integration-token.ts +++ b/backend/src/services/integration-auth/integration-token.ts @@ -2,7 +2,7 @@ import jwt from "jsonwebtoken"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { Integrations, IntegrationUrls } from "./integration-list"; @@ -234,12 +234,73 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => { }; }; -const exchangeCodeGithub = async ({ code }: { code: string }) => { +const exchangeCodeGithub = async ({ code, installationId }: { code: string; installationId?: string }) => { const appCfg = getConfig(); - if (!appCfg.CLIENT_ID_GITHUB || !appCfg.CLIENT_SECRET_GITHUB) { - throw new BadRequestError({ message: "Missing client id and client secret" }); + + if (!installationId && (!appCfg.CLIENT_ID_GITHUB || !appCfg.CLIENT_SECRET_GITHUB)) { + throw new InternalServerError({ message: "Missing client id and client secret" }); } + if (installationId && (!appCfg.CLIENT_ID_GITHUB_APP || !appCfg.CLIENT_SECRET_GITHUB_APP)) { + throw new InternalServerError({ + message: "Missing Github app client ID and client secret" + }); + } + + if (installationId) { + // handle app installations + const oauthRes = ( + await request.get(IntegrationUrls.GITHUB_TOKEN_URL, { + params: { + client_id: appCfg.CLIENT_ID_GITHUB_APP, + client_secret: appCfg.CLIENT_SECRET_GITHUB_APP, + code, + redirect_uri: `${appCfg.SITE_URL}/integrations/github/oauth2/callback` + }, + headers: { + Accept: "application/json", + "Accept-Encoding": "application/json" + } + }) + ).data; + + // use access token to validate installation ID + const installationsRes = ( + await request.get<{ + installations: { + id: number; + account: { + login: string; + }; + }[]; + }>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${oauthRes.access_token}`, + "Accept-Encoding": "application/json" + } + }) + ).data; + + const matchingInstallation = installationsRes.installations.find( + (installation) => installation.id === +installationId + ); + + if (!matchingInstallation) { + throw new ForbiddenRequestError({ + message: "User has no access to the provided installation" + }); + } + + return { + accessToken: "", // for github app integrations, we only need the installationID from the metadata + refreshToken: null, + accessExpiresAt: null, + installationName: matchingInstallation.account.login + }; + } + + // handle oauth github integration const res = ( await request.get(IntegrationUrls.GITHUB_TOKEN_URL, { params: { @@ -346,6 +407,7 @@ type TExchangeReturn = { url?: string; teamId?: string; accountId?: string; + installationName?: string; }; /** @@ -355,11 +417,13 @@ type TExchangeReturn = { export const exchangeCode = async ({ integration, code, - url + url, + installationId }: { integration: string; code: string; url?: string; + installationId?: string; }): Promise => { switch (integration) { case Integrations.GCP_SECRET_MANAGER: @@ -384,7 +448,8 @@ export const exchangeCode = async ({ }); case Integrations.GITHUB: return exchangeCodeGithub({ - code + code, + installationId }); case Integrations.GITLAB: return exchangeCodeGitlab({ diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 24e1dd5c4..47a92c384 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -258,27 +258,7 @@ export const integrationServiceFactory = ({ }); } - const deletedIntegration = await integrationDAL.transaction(async (tx) => { - // delete integration - const deletedIntegrationResult = await integrationDAL.deleteById(id, tx); - - // check if there are other integrations that share the same integration auth - const integrations = await integrationDAL.find( - { - integrationAuthId: integration.integrationAuthId - }, - tx - ); - - if (integrations.length === 0) { - // no other integration shares the same integration auth - // -> delete the integration auth - await integrationAuthDAL.deleteById(integration.integrationAuthId, tx); - } - - return deletedIntegrationResult; - }); - + const deletedIntegration = await integrationDAL.deleteById(id); return { ...integration, ...deletedIntegration }; }; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 56134bd11..24f1d55b0 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -106,16 +106,19 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("superAdmin").withSchema(TableName.Users), db.ref("publicKey").withSchema(TableName.UserEncryptionKey) ) .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER .orderBy("firstName") .orderBy("lastName"); - return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({ - ...data, - user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey } - })); + return members.map( + ({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, superAdmin, ...data }) => ({ + ...data, + user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey, superAdmin } + }) + ); } catch (error) { throw new DatabaseError({ error, name: "Find all org members" }); } diff --git a/backend/src/services/org/org-role-fns.ts b/backend/src/services/org/org-role-fns.ts index cccf255c6..f460e18a4 100644 --- a/backend/src/services/org/org-role-fns.ts +++ b/backend/src/services/org/org-role-fns.ts @@ -5,6 +5,8 @@ import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom"); +export const isCustomOrgRole = (roleSlug: string) => !RESERVED_ORG_ROLE_SLUGS.includes(roleSlug as OrgMembershipRole); + // this is only for updating an org export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ membershipRoleSlug, @@ -17,9 +19,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ orgRoleDAL: TOrgRoleDALFactory; plan: TFeatureSet; }) => { - const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(membershipRoleSlug as OrgMembershipRole); - - if (isCustomRole) { + if (isCustomOrgRole(membershipRoleSlug)) { if (!plan?.rbac) throw new BadRequestError({ message: @@ -41,9 +41,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ export const getDefaultOrgMembershipRole = async ( defaultOrgMembershipRole: string // can either be ID or reserved slug ) => { - const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(defaultOrgMembershipRole as OrgMembershipRole); - - if (isCustomRole) + if (isCustomOrgRole(defaultOrgMembershipRole)) return { roleId: defaultOrgMembershipRole, role: OrgMembershipRole.Custom diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts index 198a9ea85..f11d53aa0 100644 --- a/backend/src/services/org/org-role-service.ts +++ b/backend/src/services/org/org-role-service.ts @@ -11,6 +11,7 @@ import { } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { ActorAuthMethod } from "../auth/auth-type"; @@ -20,11 +21,17 @@ type TOrgRoleServiceFactoryDep = { orgRoleDAL: TOrgRoleDALFactory; permissionService: TPermissionServiceFactory; orgDAL: TOrgDALFactory; + externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; }; export type TOrgRoleServiceFactory = ReturnType; -export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }: TOrgRoleServiceFactoryDep) => { +export const orgRoleServiceFactory = ({ + orgRoleDAL, + orgDAL, + permissionService, + externalGroupOrgRoleMappingDAL +}: TOrgRoleServiceFactoryDep) => { const createRole = async ( userId: string, orgId: string, @@ -144,6 +151,17 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }: message: "Cannot delete default org membership role. Please re-assign and try again." }); + const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ + orgId, + roleId + }); + + if (externalGroupMapping) + throw new BadRequestError({ + message: + "Cannot delete role assigned to external group organization role mapping. Please re-assign external mapping and try again." + }); + const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId }); if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 2ee32a7d7..1ae7ce6dc 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas"; -import { ForbiddenRequestError } from "@app/lib/errors"; +import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -342,7 +342,7 @@ export const recursivelyGetSecretPaths = async ({ }); if (!env) { - throw new Error(`'${environment}' environment not found in project with ID ${projectId}`); + throw new NotFoundError({ message: `'${environment}' environment not found in project with ID ${projectId}` }); } // Fetch all folders in env once with a single query diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 3afde2f1e..1d0b89b46 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -152,7 +152,7 @@ export const recursivelyGetSecretPaths = ({ }); if (!env) { - throw new Error(`'${environment}' environment not found in project with ID ${projectId}`); + throw new NotFoundError({ message: `'${environment}' environment not found in project with ID ${projectId}` }); } // Fetch all folders in env once with a single query diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 140d96128..a7d3661b3 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -216,7 +216,9 @@ var loginCmd = &cobra.Command{ } //override domain domainQuery := true - if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL { + if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && + config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) && + config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) { overrideDomain, err := DomainOverridePrompt() if err != nil { util.HandleError(err) @@ -526,16 +528,17 @@ func askForDomain() error { // query user to choose between Infisical cloud or self hosting const ( - INFISICAL_CLOUD = "Infisical Cloud" - SELF_HOSTING = "Self Hosting" - ADD_NEW_DOMAIN = "Add a new domain" + INFISICAL_CLOUD_US = "Infisical Cloud (US Region)" + INFISICAL_CLOUD_EU = "Infisical Cloud (EU Region)" + SELF_HOSTING = "Self Hosting" + ADD_NEW_DOMAIN = "Add a new domain" ) - options := []string{INFISICAL_CLOUD, SELF_HOSTING} + options := []string{INFISICAL_CLOUD_US, INFISICAL_CLOUD_EU, SELF_HOSTING} optionsPrompt := promptui.Select{ Label: "Select your hosting option", Items: options, - Size: 2, + Size: 3, } _, selectedHostingOption, err := optionsPrompt.Run() @@ -543,10 +546,15 @@ func askForDomain() error { return err } - if selectedHostingOption == INFISICAL_CLOUD { - //cloud option - config.INFISICAL_URL = fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_URL) - config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", util.INFISICAL_DEFAULT_URL) + if selectedHostingOption == INFISICAL_CLOUD_US { + // US cloud option + config.INFISICAL_URL = fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) + config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", util.INFISICAL_DEFAULT_US_URL) + return nil + } else if selectedHostingOption == INFISICAL_CLOUD_EU { + // EU cloud option + config.INFISICAL_URL = fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) + config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", util.INFISICAL_DEFAULT_EU_URL) return nil } diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index e40c07022..c533f3415 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -40,7 +40,7 @@ func init() { cobra.OnInitialize(initLog) rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)") rootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary") - rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]") + rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL), "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]") rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.") rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { silent, err := cmd.Flags().GetBool("silent") diff --git a/cli/packages/cmd/user.go b/cli/packages/cmd/user.go index 844213e18..d3e6096a9 100644 --- a/cli/packages/cmd/user.go +++ b/cli/packages/cmd/user.go @@ -2,6 +2,7 @@ package cmd import ( "errors" + "fmt" "net/url" "github.com/Infisical/infisical-merge/packages/config" @@ -119,7 +120,7 @@ var domainCmd = &cobra.Command{ domain := "" domainQuery := true - if config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL { + if config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) && config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) { override, err := DomainOverridePrompt() if err != nil { diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index 1f9ec0c65..8b4c586e6 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -3,8 +3,8 @@ package util const ( CONFIG_FILE_NAME = "infisical-config.json" CONFIG_FOLDER_NAME = ".infisical" - INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api" - INFISICAL_DEFAULT_URL = "https://app.infisical.com" + INFISICAL_DEFAULT_US_URL = "https://app.infisical.com" + INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" diff --git a/cli/packages/util/init.go b/cli/packages/util/init.go index 33350f3b7..4aecb2ab3 100644 --- a/cli/packages/util/init.go +++ b/cli/packages/util/init.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" ) @@ -11,7 +12,7 @@ func GetOrganizationsNameList(organizationResponse api.GetOrganizationsResponse) organizations := organizationResponse.Organizations if len(organizations) == 0 { - message := fmt.Sprintf("You don't have any organization created in Infisical. You must first create a organization at %s", INFISICAL_DEFAULT_URL) + message := fmt.Sprintf("You don't have any organization created in Infisical. You must first create a organization at %s", config.INFISICAL_URL) PrintErrorMessageAndExit(message) } @@ -37,7 +38,7 @@ func GetWorkspacesInOrganization(workspaceResponse api.GetWorkSpacesResponse, or } if len(filteredWorkspaces) == 0 { - message := fmt.Sprintf("You don't have any projects created in Infisical organization. You must first create a project at %s", INFISICAL_DEFAULT_URL) + message := fmt.Sprintf("You don't have any projects created in Infisical organization. You must first create a project at %s", config.INFISICAL_URL) PrintErrorMessageAndExit(message) } diff --git a/docs/documentation/platform/admin-panel/org-admin-console.mdx b/docs/documentation/platform/admin-panel/org-admin-console.mdx index e1290fa6b..39d7819a4 100644 --- a/docs/documentation/platform/admin-panel/org-admin-console.mdx +++ b/docs/documentation/platform/admin-panel/org-admin-console.mdx @@ -1,32 +1,31 @@ --- title: "Organization Admin Console" -description: "Manage your Infisical organization from our organization admin console." +description: "View and manage resources across your organization" --- -The Organization Admin Console provides a user-friendly interface for Infisical organization admins to manage organization-related configurations. + + The Organization Admin Console can only be accessed by organization members with admin status. + + ## Accessing the Organization Admin Console -Only organization admins have access to the Organization Admin Console. +On the sidebar, tap on your initials to access the settings dropdown and press the **Organization Admin Console** option. -![Access Organization Admin Panel](/images/platform/admin-panels/access-org-admin-console.png) +![Access Organization Admin Console](/images/platform/admin-panels/access-org-admin-console.png) -1. Click on the profile icon in the left sidebar. -2. From the dropdown menu, select `Organization Admin Console`. +## Projects Tab -## Projects Section +The Projects tab lists all the projects within your organization, including those which you are not a member of. You can easily filter projects by name or slug using the search bar. ![Projects Section](/images/platform/admin-panels/org-admin-console-projects.png) -The Projects Section lists all projects created within your organization, including those you do not have membership in. You can easily search for a project by name using the search bar. ### Accessing a Project in Your Organization -If you want to access a project in which you are not a member but are an organization admin, follow these steps: +You can access a project that you are not a member of by tapping on the options menu of the project row and pressing the **Access** button. +Doing so will grant you admin permissions for the selected project and add you as a member. ![Access project](/images/platform/admin-panels/org-admin-console-access.png) -1. Click on the three-dot icon next to the project you wish to access. -2. Click on the **Access** button. -This will grant you admin permissions for the selected project and generate an audit log of your access, ensuring transparency regarding admin privileges. diff --git a/docs/documentation/platform/admin-panel/overview.mdx b/docs/documentation/platform/admin-panel/overview.mdx index 0505d6b2a..968728bfc 100644 --- a/docs/documentation/platform/admin-panel/overview.mdx +++ b/docs/documentation/platform/admin-panel/overview.mdx @@ -1,17 +1,17 @@ --- -description: "Learn about Infisical's Admin Panel." +description: "Learn about Infisical's Admin Consoles" --- -The Infisical Admin Panel allows you to configure and manage various resources within your organization and server. +Infisical offers a server and organization level console for admins to customize their settings and manage various resources across the platform. - Configure and manage your server settings effectively. + Configure and manage server related features. - Manage settings specific to your organization. + View and access resources across your organization. diff --git a/docs/documentation/platform/admin-panel/server-admin.mdx b/docs/documentation/platform/admin-panel/server-admin.mdx index 5b71109ee..355679f82 100644 --- a/docs/documentation/platform/admin-panel/server-admin.mdx +++ b/docs/documentation/platform/admin-panel/server-admin.mdx @@ -1,70 +1,69 @@ --- -title: "Server Admin Panel" -description: "Manage your Infisical server from the Server Admin Panel." +title: "Server Admin Console" +description: "Configure and manage server related features" --- -The Server Admin Panel provides a user interface for Infisical server administrators to configure various parameters as needed. This includes configuring rate limits, managing allowed signups, and more. +The Server Admin Console provides **server administrators** with the ability to +customize settings and manage users for their entire Infisical instance. -## Accessing the Server Admin Panel + + The first user to setup an account on your Infisical instance is designated as the server administrator by default. + -The first user who created the account in Infisical is designated as the server administrator. You can access the admin panel by navigating as follows: +## Accessing the Server Admin Console -![Access Server Admin Panel](/images/platform/admin-panels/access-server-admin-panel.png) -1. Click on the profile icon in the left sidebar. -2. From the dropdown menu, select `Server Admin Panel`. +On the sidebar, tap on your initials to access the settings dropdown and press the **Server Admin Console** option. -## General Section +![Access Server Admin Console](/images/platform/admin-panels/access-server-admin-panel.png) + +## General Tab +Configure general settings for your instance. ![General Settings](/images/platform/admin-panels/admin-panel-general.png) + ### Allow User Signups -This setting controls whether users can sign up for your Infisical instance. The options are: - -1. **Anyone**: Any user with access to your instance can sign up. -2. **Disabled**: No one will be able to sign up. +User signups are enabled by default, allowing **Anyone** with access to your instance to sign up. This can alternatively be **Disabled** to prevent any users from signing up. ### Restrict Signup Domain -This setting allows only users with specific email domains (such as your organization's domain) to sign up. +Signup can be restricted to users matching one or more email domains, such as your organization's domain, to control who has access to your instance. ### Default Organization -Use this setting if you want all users accessing your Infisical instance to log in through your configured SAML/LDAP provider. This prevents users from manually entering their organization slug during authentication and redirects them to the SAML/LDAP authentication page. +If you're using SAML/LDAP for only one organization on your instance, you can specify a default organization to use at login to skip requiring users to manually enter the organization slug. ### Trust Emails -By default, Infisical does not trust emails logged in via SAML/LDAP/OIDC due to the potential for email spoofing. Users must verify their email addresses before proceeding. You can disable this validation if you are running an Infisical instance within your organization and trust incoming emails from your members. +By default, users signing up through SAML/LDAP/OIDC will still need to verify their email address to prevent email spoofing. This requirement can be skipped by enabling the switch to trust logins through the respective method. -## Authentication Section + +## Authentication Tab + +From this tab, you can configure which login methods are enabled for your instance. ![Authentication Settings](/images/platform/admin-panels/admin-panel-auths.png) -This section allows you to configure various login and signup methods for your instance. -## Rate Limit Section +## Rate Limit Tab + +This tab allows you to set various rate limits for your Infisical instance. You do not need to redeploy when making changes to rate limits as these will be propagated automatically. ![Rate Limit Settings](/images/platform/admin-panels/admin-panel-rate-limits.png) -Configure the rate limits for your Infisical instance across various endpoints. You do not need to redeploy when making changes to rate limits; they will be automatically synchronized to all instances. - + Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use. - + -## User Management Section +## User Management Tab + +From this tab, you can view all the users who have signed up for your instance. You can search for users using the search bar and remove them from your instance by pressing the **X** button on their respective row. ![User Management](/images/platform/admin-panels/admin-panel-users.png) -The User Management section lists all users who have signed up for your instance. You can search for users using the search bar. - -To delete a user from Infisical: - -1. Search for the user. -2. Click the cross button next to the user. -3. Confirm the warning popup. - - - Note that user management configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use. - + + Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use. + diff --git a/docs/documentation/platform/ldap/overview.mdx b/docs/documentation/platform/ldap/overview.mdx index 4502158d0..c1a980b04 100644 --- a/docs/documentation/platform/ldap/overview.mdx +++ b/docs/documentation/platform/ldap/overview.mdx @@ -36,7 +36,7 @@ If the documentation for your required identity provider is not shown in the lis verification step upon their first login. If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers, - you can configure this behavior in the admin panel. + you can configure this behavior in the Server Admin Console. diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx index ff46fe4e7..2c48e386d 100644 --- a/docs/documentation/platform/scim/azure.mdx +++ b/docs/documentation/platform/scim/azure.mdx @@ -39,7 +39,7 @@ Prerequisites: - Tenant URL: Input **SCIM URL** from Step 1. - Secret Token: Input the **New SCIM Token** from Step 1. - Afterwards, press the **Test Connection** button to check that SCIM is configured properly. + Afterwards, click **Enable SCIM** and press the **Test Connection** button to check that SCIM is configured properly. ![SCIM Azure](/images/platform/scim/azure/scim-azure-config.png) @@ -71,4 +71,4 @@ Prerequisites: For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets. - \ No newline at end of file + diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 4bb45cf48..227a7502f 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -45,7 +45,7 @@ If your required identity provider is not shown in the list above, please reach verification step upon their first login. If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers, - you can configure this behavior in the admin panel. + you can configure this behavior in the Server Admin Console. diff --git a/docs/images/integrations/github/app/github-app-installation.png b/docs/images/integrations/github/app/github-app-installation.png new file mode 100644 index 000000000..60a2ec4fc Binary files /dev/null and b/docs/images/integrations/github/app/github-app-installation.png differ diff --git a/docs/images/integrations/github/app/github-app-method-selection.png b/docs/images/integrations/github/app/github-app-method-selection.png new file mode 100644 index 000000000..3f66a396e Binary files /dev/null and b/docs/images/integrations/github/app/github-app-method-selection.png differ diff --git a/docs/images/integrations/github/app/integration-overview.png b/docs/images/integrations/github/app/integration-overview.png new file mode 100644 index 000000000..1dad2fb64 Binary files /dev/null and b/docs/images/integrations/github/app/integration-overview.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-basic-details.png b/docs/images/integrations/github/app/self-hosted-github-app-basic-details.png new file mode 100644 index 000000000..463adabd8 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-basic-details.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-create-confirm.png b/docs/images/integrations/github/app/self-hosted-github-app-create-confirm.png new file mode 100644 index 000000000..15dc7f9d6 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-create-confirm.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-create.png b/docs/images/integrations/github/app/self-hosted-github-app-create.png new file mode 100644 index 000000000..d55a49b66 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-create.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-credentials.png b/docs/images/integrations/github/app/self-hosted-github-app-credentials.png new file mode 100644 index 000000000..6e4480bc7 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-credentials.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-enable-oauth.png b/docs/images/integrations/github/app/self-hosted-github-app-enable-oauth.png new file mode 100644 index 000000000..45d50c7b2 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-enable-oauth.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-organization.png b/docs/images/integrations/github/app/self-hosted-github-app-organization.png new file mode 100644 index 000000000..60ba84151 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-organization.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-private-key.png b/docs/images/integrations/github/app/self-hosted-github-app-private-key.png new file mode 100644 index 000000000..ce03f740e Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-private-key.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-repository.png b/docs/images/integrations/github/app/self-hosted-github-app-repository.png new file mode 100644 index 000000000..edf1d1087 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-repository.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-secret.png b/docs/images/integrations/github/app/self-hosted-github-app-secret.png new file mode 100644 index 000000000..8c9918404 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-secret.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-webhook.png b/docs/images/integrations/github/app/self-hosted-github-app-webhook.png new file mode 100644 index 000000000..2b7493fbf Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-webhook.png differ diff --git a/docs/images/integrations/github/github-oauth-method-selection.png b/docs/images/integrations/github/github-oauth-method-selection.png new file mode 100644 index 000000000..eb1d00cfc Binary files /dev/null and b/docs/images/integrations/github/github-oauth-method-selection.png differ diff --git a/docs/images/integrations/github/integration-overview.png b/docs/images/integrations/github/integration-overview.png new file mode 100644 index 000000000..1dad2fb64 Binary files /dev/null and b/docs/images/integrations/github/integration-overview.png differ diff --git a/docs/images/platform/admin-panels/access-org-admin-console.png b/docs/images/platform/admin-panels/access-org-admin-console.png index fc4b3dcc1..057c82944 100644 Binary files a/docs/images/platform/admin-panels/access-org-admin-console.png and b/docs/images/platform/admin-panels/access-org-admin-console.png differ diff --git a/docs/images/platform/admin-panels/access-server-admin-panel.png b/docs/images/platform/admin-panels/access-server-admin-panel.png index 65402a176..a27735de0 100644 Binary files a/docs/images/platform/admin-panels/access-server-admin-panel.png and b/docs/images/platform/admin-panels/access-server-admin-panel.png differ diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx index 607a58d36..8f009716c 100644 --- a/docs/integrations/cicd/githubactions.mdx +++ b/docs/integrations/cicd/githubactions.mdx @@ -4,12 +4,122 @@ description: "How to sync secrets from Infisical to GitHub Actions" --- - Alternatively, you can use Infisical's official Github Action + Alternatively, you can use Infisical's official GitHub Action [here](https://github.com/Infisical/secrets-action). Infisical lets you sync secrets to GitHub at the organization-level, repository-level, and repository environment-level. +## Connecting with GitHub App (Recommended) + + + + + + Navigate to your project's integrations tab in Infisical and press on the GitHub tile. + + ![integrations](../../images/integrations/github/app/integration-overview.png) + + Select GitHub App as the authentication method and click **Connect to GitHub**. + + ![integrations github app auth selection](../../images/integrations/github/app/github-app-method-selection.png) + + You will then be redirected to the GitHub app installation page. + + ![integrations github app installation](../../images/integrations/github/app/github-app-installation.png) + + Install and authorize the GitHub application. This will redirect you back to the Infisical integration page. + + + + Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment. + + + + ![integrations github](../../images/integrations/github/integrations-github-scope-repo.png) + + + ![integrations github](../../images/integrations/github/integrations-github-scope-org.png) + + When using the organization scope, your secrets will be saved in the top-level of your GitHub Organization. + + You can choose the visibility, which defines which repositories can access the secrets. The options are: + - **All public repositories**: All public repositories in the organization can access the secrets. + - **All private repositories**: All private repositories in the organization can access the secrets. + - **Selected repositories**: Only the selected repositories can access the secrets. This gives a more fine-grained control over which repositories can access the secrets. You can select _both_ private and public repositories with this option. + + + ![integrations github](../../images/integrations/github/integrations-github-scope-env.png) + + + + Finally, press create integration to start syncing secrets to GitHub. + + ![integrations github](../../images/integrations/github/integrations-github.png) + + + + + + Using the GitHub integration with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub + and registering your instance with it. + + + Navigate to the GitHub app settings [here](https://github.com/settings/apps). Click **New GitHub App**. + + ![integrations github app create](../../images/integrations/github/app/self-hosted-github-app-create.png) + + Give the application a name, a homepage URL (your self-hosted domain i.e. `https://your-domain.com`), and a callback URL (i.e. `https://your-domain.com/integrations/github/oauth2/callback`). + + ![integrations github app basic details](../../images/integrations/github/app/self-hosted-github-app-basic-details.png) + + Enable request user authorization during app installation. + ![integrations github app enable auth](../../images/integrations/github/app/self-hosted-github-app-enable-oauth.png) + + Disable webhook by unchecking the Active checkbox. + ![integrations github app webhook](../../images/integrations/github/app/self-hosted-github-app-webhook.png) + + Set the repository permissions as follows: Metadata: Read-only, Secrets: Read and write, Environments: Read and write. + ![integrations github app repository](../../images/integrations/github/app/self-hosted-github-app-repository.png) + + Similarly, set the organization permissions as follows: Secrets: Read and write. + ![integrations github app organization](../../images/integrations/github/app/self-hosted-github-app-organization.png) + + Create the Github application. + ![integrations github app create confirm](../../images/integrations/github/app/self-hosted-github-app-create-confirm.png) + + + If you have a GitHub organization, you can create an application under it + in your organization Settings > Developer settings > GitHub Apps > New GitHub App. + + + + Generate a new **Client Secret** for your GitHub application. + ![integrations github app create secret](../../images/integrations/github/app/self-hosted-github-app-secret.png) + + Generate a new **Private Key** for your Github application. + ![integrations github app create private key](../../images/integrations/github/app/self-hosted-github-app-private-key.png) + + Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key. + ![integrations github app credentials](../../images/integrations/github/app/self-hosted-github-app-credentials.png) + + Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application: + + - `CLIENT_ID_GITHUB_APP`: The **Client ID** of your GitHub application. + - `CLIENT_SECRET_GITHUB_APP`: The **Client Secret** of your GitHub application. + - `CLIENT_SLUG_GITHUB_APP`: The **Slug** of your GitHub application. This is the one found in the URL. + - `CLIENT_APP_ID_GITHUB_APP`: The **App ID** of your GitHub application. + - `CLIENT_PRIVATE_KEY_GITHUB_APP`: The **Private Key** of your GitHub application. + + Once added, restart your Infisical instance and use the GitHub integration via app authentication. + + + + + + +## Connecting with GitHub OAuth + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) @@ -19,12 +129,13 @@ Prerequisites: - Navigate to your project's integrations tab in Infisical. + Navigate to your project's integrations tab in Infisical and press on the GitHub tile. + ![integrations](../../images/integrations/github/integration-overview.png) - ![integrations](../../images/integrations.png) - - Press on the GitHub tile and grant Infisical access to your GitHub account (repo privileges only). + Select OAuth as the authentication method and click **Connect to GitHub**. + ![integrations github oauth auth selection](../../images/integrations/github/github-oauth-method-selection.png) + Grant Infisical access to your GitHub account (organization and repo privileges). ![integrations github authorization](../../images/integrations/github/integrations-github-auth.png) @@ -38,7 +149,7 @@ Prerequisites: ![integrations github](../../images/integrations/github/integrations-github-scope-org.png) - When using the organization scope, your secrets will be saved in the top-level of your Github Organization. + When using the organization scope, your secrets will be saved in the top-level of your GitHub Organization. You can choose the visibility, which defines which repositories can access the secrets. The options are: - **All public repositories**: All public repositories in the organization can access the secrets. diff --git a/docs/mint.json b/docs/mint.json index 261bcd746..0d7553b56 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -188,7 +188,7 @@ ] }, { - "group": "Admin Panel", + "group": "Admin Consoles", "pages": [ "documentation/platform/admin-panel/overview", "documentation/platform/admin-panel/server-admin", diff --git a/frontend/src/helpers/roles.ts b/frontend/src/helpers/roles.ts new file mode 100644 index 000000000..afb9e1274 --- /dev/null +++ b/frontend/src/helpers/roles.ts @@ -0,0 +1,8 @@ +enum OrgMembershipRole { + Admin = "admin", + Member = "member", + NoAccess = "no-access" +} + +export const isCustomOrgRole = (slug: string) => + !Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole); diff --git a/frontend/src/hooks/api/externalGroupOrgRoleMappings/index.ts b/frontend/src/hooks/api/externalGroupOrgRoleMappings/index.ts new file mode 100644 index 000000000..177955438 --- /dev/null +++ b/frontend/src/hooks/api/externalGroupOrgRoleMappings/index.ts @@ -0,0 +1,3 @@ +export * from "./mutations"; +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/externalGroupOrgRoleMappings/mutations.tsx b/frontend/src/hooks/api/externalGroupOrgRoleMappings/mutations.tsx new file mode 100644 index 000000000..aac46726a --- /dev/null +++ b/frontend/src/hooks/api/externalGroupOrgRoleMappings/mutations.tsx @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { externalGroupOrgRoleMappingKeys } from "@app/hooks/api/externalGroupOrgRoleMappings/queries"; +import { TSyncExternalGroupOrgRoleMappingsDTO } from "@app/hooks/api/externalGroupOrgRoleMappings/types"; + +export const useUpdateExternalGroupOrgRoleMappings = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: TSyncExternalGroupOrgRoleMappingsDTO) => { + const { data } = await apiRequest.put("/api/v1/external-group-mappings", payload); + + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries(externalGroupOrgRoleMappingKeys.list()); + } + }); +}; diff --git a/frontend/src/hooks/api/externalGroupOrgRoleMappings/queries.tsx b/frontend/src/hooks/api/externalGroupOrgRoleMappings/queries.tsx new file mode 100644 index 000000000..620ec0447 --- /dev/null +++ b/frontend/src/hooks/api/externalGroupOrgRoleMappings/queries.tsx @@ -0,0 +1,33 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { TExternalGroupOrgRoleMappingList } from "@app/hooks/api/externalGroupOrgRoleMappings/types"; + +export const externalGroupOrgRoleMappingKeys = { + all: ["external-group-org-role-mapping"] as const, + list: () => [...externalGroupOrgRoleMappingKeys.all, "list"] as const +}; + +export const useGetExternalGroupOrgRoleMappings = ( + options?: Omit< + UseQueryOptions< + TExternalGroupOrgRoleMappingList, + unknown, + TExternalGroupOrgRoleMappingList, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: externalGroupOrgRoleMappingKeys.list(), + queryFn: async () => { + const { data } = await apiRequest.get( + "/api/v1/external-group-mappings" + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/externalGroupOrgRoleMappings/types.ts b/frontend/src/hooks/api/externalGroupOrgRoleMappings/types.ts new file mode 100644 index 000000000..313045742 --- /dev/null +++ b/frontend/src/hooks/api/externalGroupOrgRoleMappings/types.ts @@ -0,0 +1,18 @@ +export type TSyncExternalGroupOrgRoleMappingsDTO = { + mappings: { + groupName: string; + roleSlug: string; + }[]; +}; + +export type TExternalGroupOrgRoleMapping = { + id: string; + groupName: string; + role: string; + roleId: string; + orgId: string; + createdAt: string; + updatedAt: string; +}; + +export type TExternalGroupOrgRoleMappingList = TExternalGroupOrgRoleMapping[]; diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index 5f0503eee..0ae3511de 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -1,3 +1,4 @@ +export { useDuplicateIntegrationAuth } from "./mutations"; export { useAuthorizeIntegration, useDeleteIntegrationAuth, diff --git a/frontend/src/hooks/api/integrationAuth/mutations.tsx b/frontend/src/hooks/api/integrationAuth/mutations.tsx new file mode 100644 index 000000000..b7a3f18bd --- /dev/null +++ b/frontend/src/hooks/api/integrationAuth/mutations.tsx @@ -0,0 +1,19 @@ +import { useMutation } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { IntegrationAuth, TDuplicateIntegrationAuthDTO } from "./types"; + +// For now, this should only be used in the Github app integration flow. +export const useDuplicateIntegrationAuth = () => { + return useMutation({ + mutationFn: async (body) => { + const { data } = await apiRequest.post<{ integrationAuth: IntegrationAuth }>( + `/api/v1/integration-auth/${body.integrationAuthId}/duplicate`, + body + ); + + return data.integrationAuth; + } + }); +}; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 914c3a71c..6688f9a99 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -777,11 +777,13 @@ export const useAuthorizeIntegration = () => { workspaceId, code, integration, + installationId, url }: { workspaceId: string; code: string; integration: string; + installationId?: string; url?: string; }) => { const { @@ -790,6 +792,7 @@ export const useAuthorizeIntegration = () => { workspaceId, code, integration, + installationId, url }); diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index b73528384..9a8ca0e31 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -9,6 +9,10 @@ export type IntegrationAuth = { keyEncoding: string; url?: string; teamId?: string; + metadata: { + installationName?: string; + installationId?: string; + }; }; export type App = { @@ -84,3 +88,8 @@ export type TeamCityBuildConfig = { name: string; buildConfigId: string; }; + +export type TDuplicateIntegrationAuthDTO = { + integrationAuthId: string; + projectId: string; +}; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index f7a898ef0..fece19e5f 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -8,8 +8,9 @@ export { useDeleteOrgTaxId, useGetIdentityMembershipOrgs, useGetOrganizationGroups, - useGetOrganizations, + useGetOrganizations, useGetOrgBillingDetails, + useGetOrgIntegrationAuths, useGetOrgInvoices, useGetOrgLicenses, useGetOrgPlanBillingInfo, @@ -20,4 +21,4 @@ export { useGetOrgTrialUrl, useUpdateOrg, useUpdateOrgBillingDetails -} from "./queries"; \ No newline at end of file +} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index caab2408d..ad44d280a 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -4,6 +4,7 @@ import { apiRequest } from "@app/config/request"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { TGroupOrgMembership } from "../groups/types"; +import { IntegrationAuth } from "../types"; import { BillingDetails, Invoice, @@ -39,7 +40,8 @@ export const organizationKeys = { ...params }: TListOrgIdentitiesDTO) => [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, - getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const + getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, + getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const }; export const fetchOrganizations = async () => { @@ -473,3 +475,21 @@ export const useGetOrganizationGroups = (organizationId: string) => { } }); }; + +export const useGetOrgIntegrationAuths = ( + organizationId: string, + select?: (data: IntegrationAuth[]) => TData +) => { + return useQuery({ + queryKey: organizationKeys.getOrgIntegrationAuths(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>( + `/api/v1/organization/${organizationId}/integration-authorizations` + ); + + return data.authorizations; + }, + enabled: Boolean(organizationId), + select + }); +}; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index c22bc216f..84566efcb 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -56,6 +56,7 @@ export type OrgUser = { lastName: string; id: string; publicKey: string; + superAdmin: boolean; }; inviteEmail: string; organization: string; diff --git a/frontend/src/layouts/AdminLayout/AdminLayout.tsx b/frontend/src/layouts/AdminLayout/AdminLayout.tsx index f806b6e79..a8320a525 100644 --- a/frontend/src/layouts/AdminLayout/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout/AdminLayout.tsx @@ -157,7 +157,7 @@ export const AdminLayout = ({ children }: LayoutProps) => { {user?.superAdmin && ( - Admin Panel + Server Admin Console )} diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index f0737c32d..e911fbb5a 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -510,7 +510,7 @@ export const AppLayout = ({ children }: LayoutProps) => { {user?.superAdmin && ( - Server Admin Panel + Server Admin Console )} diff --git a/frontend/src/pages/integrations/github/auth-mode-selection.tsx b/frontend/src/pages/integrations/github/auth-mode-selection.tsx new file mode 100644 index 000000000..5fcb4f0dd --- /dev/null +++ b/frontend/src/pages/integrations/github/auth-mode-selection.tsx @@ -0,0 +1,108 @@ +import crypto from "crypto"; + +import { useState } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + Card, + CardBody, + CardTitle, + FormControl, + Select, + SelectItem +} from "@app/components/v2"; +import { useGetCloudIntegrations } from "@app/hooks/api"; + +enum AuthMethod { + APP = "APP", + OAUTH = "OAUTH" +} + +export default function GithubIntegrationAuthModeSelectionPage() { + const router = useRouter(); + const { data: cloudIntegrations } = useGetCloudIntegrations(); + const githubIntegration = cloudIntegrations?.find((integration) => integration.slug === "github"); + const [selectedAuthMethod, setSelectedAuthMethod] = useState(AuthMethod.APP); + + return ( +
+ + Select GitHub Integration Auth + + + + +
+
+ Github logo +
+ GitHub Integration + + +
+ + Docs + +
+
+ +
+
+ + + + +
+ +
+
+
+
+ ); +} + +GithubIntegrationAuthModeSelectionPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx index c002ea103..9cacba01b 100644 --- a/frontend/src/pages/integrations/github/create.tsx +++ b/frontend/src/pages/integrations/github/create.tsx @@ -478,7 +478,7 @@ export default function GitHubCreateIntegrationPage() { errorText={ integrationAuthOrgs?.length ? error?.message : "No organizations found" } - isError={Boolean(integrationAuthOrgs?.length && error?.message)} + isError={Boolean(integrationAuthOrgs?.length || error?.message)} > + + )} + /> + +
+ {i === 0 && ( + + Role to Assign Users in this Group + + )} + ( + + + + )} + /> +
+ { + mappingField.remove(i); + }} + > + + + + ))} +
+ +
+ {isDirty && ( +
+ + {(isAllowed) => ( + + )} + +
+ )} + + )} + + + ); +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx index ba216ff3a..30cb16293 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx @@ -9,6 +9,7 @@ import { } from "@app/context"; import { useUpdateOrg } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { ExternalGroupOrgRoleMappings } from "@app/views/Settings/OrgSettingsPage/components/OrgAuthTab/ExternalGroupOrgRoleMappings"; import { ScimTokenModal } from "./ScimTokenModal"; @@ -76,6 +77,7 @@ export const OrgScimSection = () => {

Manage SCIM configuration

+

Enable SCIM

diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx index 8b5d52d31..b24f2a205 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx @@ -12,8 +12,8 @@ import { useOrganization, useOrgPermission } from "@app/context"; +import { isCustomOrgRole } from "@app/helpers/roles"; import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api"; -import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable"; const formSchema = yup.object({ name: yup diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 2bd0856f0..08225535e 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -125,7 +125,7 @@ export const AdminDashboardPage = () => {
-

Admin Dashboard

+

Server Admin Console

Manage your instance level configurations.

diff --git a/frontend/src/views/admin/DashboardPage/UserPanel.tsx b/frontend/src/views/admin/DashboardPage/UserPanel.tsx index 672f7f663..29fa8d4f6 100644 --- a/frontend/src/views/admin/DashboardPage/UserPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/UserPanel.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { + Badge, Button, DeleteActionModal, EmptyState, @@ -69,12 +70,19 @@ const UserPanelTable = ({ {isLoading && } {!isLoading && data?.pages?.map((users) => - users.map(({ username, email, firstName, lastName, id }) => { + users.map(({ username, email, firstName, lastName, id, superAdmin }) => { const name = firstName || lastName ? `${firstName} ${lastName}` : "-"; return ( - {name} + + {name} + {superAdmin && ( + + Server Admin + + )} + {email} {userId !== id && (