From f720a9afb2c45636ce35b276b8d9afdcec2d250f Mon Sep 17 00:00:00 2001 From: Samuel Torres Date: Mon, 24 Mar 2025 00:25:20 -0700 Subject: [PATCH] feat: validate github organization during OAuth login (#6700) * feat: add GITHUB_ORGANIZATION_ID support for GitHub OAuth integration * fix: remove debug print statements from InstanceConfigurationEndpoint --- admin/app/authentication/github/form.tsx | 15 ++++++++++ .../plane/authentication/adapter/error.py | 2 ++ .../authentication/provider/oauth/github.py | 29 ++++++++++++++++++- .../management/commands/configure_instance.py | 6 ++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/admin/app/authentication/github/form.tsx b/admin/app/authentication/github/form.tsx index 4842675cd..91795ea70 100644 --- a/admin/app/authentication/github/form.tsx +++ b/admin/app/authentication/github/form.tsx @@ -43,6 +43,7 @@ export const InstanceGithubConfigForm: FC = (props) => { defaultValues: { GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"], GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"], + GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"], }, }); @@ -93,6 +94,19 @@ export const InstanceGithubConfigForm: FC = (props) => { error: Boolean(errors.GITHUB_CLIENT_SECRET), required: true, }, + { + key: "GITHUB_ORGANIZATION_ID", + type: "text", + label: "Organization ID", + description: ( + <> + The organization github ID. + + ), + placeholder: "123456789", + error: Boolean(errors.GITHUB_ORGANIZATION_ID), + required: false, + }, ]; const GITHUB_SERVICE_FIELD: TCopyField[] = [ @@ -150,6 +164,7 @@ export const InstanceGithubConfigForm: FC = (props) => { reset({ GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value, GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value, + GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value, }); }) .catch((err) => console.error(err)); diff --git a/apiserver/plane/authentication/adapter/error.py b/apiserver/plane/authentication/adapter/error.py index 63fafffbe..dcbe039fb 100644 --- a/apiserver/plane/authentication/adapter/error.py +++ b/apiserver/plane/authentication/adapter/error.py @@ -36,10 +36,12 @@ AUTHENTICATION_ERROR_CODES = { "OAUTH_NOT_CONFIGURED": 5104, "GOOGLE_NOT_CONFIGURED": 5105, "GITHUB_NOT_CONFIGURED": 5110, + "GITHUB_USER_NOT_IN_ORG": 5122, "GITLAB_NOT_CONFIGURED": 5111, "GOOGLE_OAUTH_PROVIDER_ERROR": 5115, "GITHUB_OAUTH_PROVIDER_ERROR": 5120, "GITLAB_OAUTH_PROVIDER_ERROR": 5121, + # Reset Password "INVALID_PASSWORD_TOKEN": 5125, "EXPIRED_PASSWORD_TOKEN": 5130, diff --git a/apiserver/plane/authentication/provider/oauth/github.py b/apiserver/plane/authentication/provider/oauth/github.py index 1808aa515..4a7808c8a 100644 --- a/apiserver/plane/authentication/provider/oauth/github.py +++ b/apiserver/plane/authentication/provider/oauth/github.py @@ -18,11 +18,16 @@ from plane.authentication.adapter.error import ( class GitHubOAuthProvider(OauthAdapter): token_url = "https://github.com/login/oauth/access_token" userinfo_url = "https://api.github.com/user" + org_membership_url = f"https://api.github.com/orgs" + provider = "github" scope = "read:user user:email" + organization_scope = "read:org" + + def __init__(self, request, code=None, state=None, callback=None): - GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET = get_configuration_value( + GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = get_configuration_value( [ { "key": "GITHUB_CLIENT_ID", @@ -32,6 +37,10 @@ class GitHubOAuthProvider(OauthAdapter): "key": "GITHUB_CLIENT_SECRET", "default": os.environ.get("GITHUB_CLIENT_SECRET"), }, + { + "key": "GITHUB_ORGANIZATION_ID", + "default": os.environ.get("GITHUB_ORGANIZATION_ID"), + }, ] ) @@ -43,6 +52,10 @@ class GitHubOAuthProvider(OauthAdapter): client_id = GITHUB_CLIENT_ID client_secret = GITHUB_CLIENT_SECRET + self.organization_id = GITHUB_ORGANIZATION_ID + + if self.organization_id: + self.scope += f" {self.organization_scope}" redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/""" url_params = { @@ -113,12 +126,26 @@ class GitHubOAuthProvider(OauthAdapter): error_message="GITHUB_OAUTH_PROVIDER_ERROR", ) + def is_user_in_organization(self, github_username): + headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"} + response = requests.get(f"{self.org_membership_url}/{self.organization_id}/memberships/{github_username}", headers=headers) + return response.status_code == 200 # 200 means the user is a member + def set_user_data(self): user_info_response = self.get_user_response() headers = { "Authorization": f"Bearer {self.token_data.get('access_token')}", "Accept": "application/json", } + + if self.organization_id: + if not self.is_user_in_organization(user_info_response.get("login")): + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["GITHUB_USER_NOT_IN_ORG"], + error_message="GITHUB_USER_NOT_IN_ORG", + ) + + email = self.__get_email(headers=headers) super().set_user_data( { diff --git a/apiserver/plane/license/management/commands/configure_instance.py b/apiserver/plane/license/management/commands/configure_instance.py index 8458df5df..ce6bbf7a0 100644 --- a/apiserver/plane/license/management/commands/configure_instance.py +++ b/apiserver/plane/license/management/commands/configure_instance.py @@ -71,6 +71,12 @@ class Command(BaseCommand): "category": "GITHUB", "is_encrypted": True, }, + { + "key": "GITHUB_ORGANIZATION_ID", + "value": os.environ.get("GITHUB_ORGANIZATION_ID"), + "category": "GITHUB", + "is_encrypted": False, + }, { "key": "GITLAB_HOST", "value": os.environ.get("GITLAB_HOST"),