From 9f16352188d0889bd050c80b20c4aaf325e49fd7 Mon Sep 17 00:00:00 2001 From: Harold Wanyama Date: Tue, 7 May 2024 19:45:43 +0300 Subject: [PATCH] Feature/Co-author Invalid email - Handled co-author with invalid email use case (missing ID) Signed-off-by: Harold Wanyama --- cla-backend/cla/models/github_models.py | 1203 +++++++++-------- .../cla/tests/unit/test_github_models.py | 301 +---- cla-backend/cla/utils.py | 855 ++++++------ 3 files changed, 1180 insertions(+), 1179 deletions(-) diff --git a/cla-backend/cla/models/github_models.py b/cla-backend/cla/models/github_models.py index 6bbbb5792..73d0688f1 100644 --- a/cla-backend/cla/models/github_models.py +++ b/cla-backend/cla/models/github_models.py @@ -4,26 +4,29 @@ """ Holds the GitHub repository service. """ +import concurrent.futures import json import os +import threading import time import uuid -import concurrent.futures -from typing import List, Union, Optional +from typing import List, Optional, Union -import threading +import cla import falcon import github -from github import PullRequest -from github.GithubException import UnknownObjectException, BadCredentialsException, GithubException, IncompletableObject, RateLimitExceededException -from requests_oauthlib import OAuth2Session - -import cla from cla.controllers.github_application import GitHubInstallation -from cla.models import repository_service_interface, DoesNotExist -from cla.models.dynamo_models import Repository, GitHubOrg -from cla.utils import get_project_instance, append_project_version_to_url, set_active_pr_metadata +from cla.models import DoesNotExist, repository_service_interface +from cla.models.dynamo_models import GitHubOrg, Repository from cla.user import UserCommitSummary +from cla.utils import (append_project_version_to_url, get_project_instance, + set_active_pr_metadata) +from github import PullRequest +from github.GithubException import (BadCredentialsException, GithubException, + IncompletableObject, + RateLimitExceededException, + UnknownObjectException) +from requests_oauthlib import OAuth2Session # some emails we want to exclude when we register the users EXCLUDE_GITHUB_EMAILS = ["noreply.github.com"] @@ -62,93 +65,94 @@ def get_repository_id(self, repo_name, installation_id=None): try: return self.client.get_repo(repo_name).id except github.GithubException as err: - cla.log.error('Could not find GitHub repository (%s), ensure it exists and that ' - 'your personal access token is configured with the repo scope', repo_name) + cla.log.error( + "Could not find GitHub repository (%s), ensure it exists and that " + "your personal access token is configured with the repo scope", + repo_name, + ) except Exception as err: - cla.log.error('Unknown error while getting GitHub repository ID for repository %s: %s', - repo_name, str(err)) + cla.log.error("Unknown error while getting GitHub repository ID for repository %s: %s", repo_name, str(err)) def received_activity(self, data): - cla.log.debug('github_models.received_activity - Received GitHub activity: %s', data) - if 'pull_request' not in data and 'merge_group' not in data: - cla.log.debug('github_models.received_activity - Activity not related to pull request - ignoring') - return {'message': 'Not a pull request nor a merge group - no action performed'} - if data['action'] == 'opened': - cla.log.debug('github_models.received_activity - Handling opened pull request') + cla.log.debug("github_models.received_activity - Received GitHub activity: %s", data) + if "pull_request" not in data and "merge_group" not in data: + cla.log.debug("github_models.received_activity - Activity not related to pull request - ignoring") + return {"message": "Not a pull request nor a merge group - no action performed"} + if data["action"] == "opened": + cla.log.debug("github_models.received_activity - Handling opened pull request") return self.process_opened_pull_request(data) - elif data['action'] == 'reopened': - cla.log.debug('github_models.received_activity - Handling reopened pull request') + elif data["action"] == "reopened": + cla.log.debug("github_models.received_activity - Handling reopened pull request") return self.process_reopened_pull_request(data) - elif data['action'] == 'closed': - cla.log.debug('github_models.received_activity - Handling closed pull request') + elif data["action"] == "closed": + cla.log.debug("github_models.received_activity - Handling closed pull request") return self.process_closed_pull_request(data) - elif data['action'] == 'synchronize': - cla.log.debug('github_models.received_activity - Handling synchronized pull request') + elif data["action"] == "synchronize": + cla.log.debug("github_models.received_activity - Handling synchronized pull request") return self.process_synchronized_pull_request(data) - elif data['action'] == 'checks_requested': - cla.log.debug('github_models.received_activity - Handling checks requested pull request') + elif data["action"] == "checks_requested": + cla.log.debug("github_models.received_activity - Handling checks requested pull request") return self.process_checks_requested_merge_group(data) else: - cla.log.debug('github_models.received_activity - Ignoring unsupported action: {}'.format(data['action'])) + cla.log.debug("github_models.received_activity - Ignoring unsupported action: {}".format(data["action"])) def sign_request(self, installation_id, github_repository_id, change_request_id, request): """ This method gets called when the OAuth2 app (NOT the GitHub App) needs to get info on the user trying to sign. In this case we begin an OAuth2 exchange with the 'user:email' scope. """ - fn = 'github_models.sign_request' # function name - cla.log.debug(f'{fn} - Initiating GitHub sign request for installation_id: {installation_id}, ' - f'for repository {github_repository_id}, ' - f'for PR: {change_request_id}') + fn = "github_models.sign_request" # function name + cla.log.debug( + f"{fn} - Initiating GitHub sign request for installation_id: {installation_id}, " + f"for repository {github_repository_id}, " + f"for PR: {change_request_id}" + ) # Not sure if we need a different token for each installation ID... - cla.log.debug(f'{fn} - Loading session from request: {request}...') + cla.log.debug(f"{fn} - Loading session from request: {request}...") session = self._get_request_session(request) - cla.log.debug(f'{fn} - Adding github details to session: {session} which is type: {type(session)}...') - session['github_installation_id'] = installation_id - session['github_repository_id'] = github_repository_id - session['github_change_request_id'] = change_request_id + cla.log.debug(f"{fn} - Adding github details to session: {session} which is type: {type(session)}...") + session["github_installation_id"] = installation_id + session["github_repository_id"] = github_repository_id + session["github_change_request_id"] = change_request_id - cla.log.debug(f'{fn} - Determining return URL from the inbound request...') + cla.log.debug(f"{fn} - Determining return URL from the inbound request...") origin_url = self.get_return_url(github_repository_id, change_request_id, installation_id) - cla.log.debug(f'{fn} - Return URL from the inbound request is {origin_url}') - session['github_origin_url'] = origin_url + cla.log.debug(f"{fn} - Return URL from the inbound request is {origin_url}") + session["github_origin_url"] = origin_url cla.log.debug(f'{fn} - Stored origin url in session as session["github_origin_url"] = {origin_url}') - if 'github_oauth2_token' in session: - cla.log.debug(f'{fn} - Using existing session GitHub OAuth2 token') - return self.redirect_to_console( - installation_id, github_repository_id, change_request_id, - origin_url, request) + if "github_oauth2_token" in session: + cla.log.debug(f"{fn} - Using existing session GitHub OAuth2 token") + return self.redirect_to_console(installation_id, github_repository_id, change_request_id, origin_url, request) else: - cla.log.debug(f'{fn} - No existing GitHub OAuth2 token - building authorization url and state') - authorization_url, state = self.get_authorization_url_and_state(installation_id, - github_repository_id, - int(change_request_id), - ['user:email']) - cla.log.debug(f'{fn} - Obtained GitHub OAuth2 state from authorization - storing state in the session...') - session['github_oauth2_state'] = state - cla.log.debug(f'{fn} - GitHub OAuth2 request with state {state} - sending user to {authorization_url}') + cla.log.debug(f"{fn} - No existing GitHub OAuth2 token - building authorization url and state") + authorization_url, state = self.get_authorization_url_and_state( + installation_id, github_repository_id, int(change_request_id), ["user:email"] + ) + cla.log.debug(f"{fn} - Obtained GitHub OAuth2 state from authorization - storing state in the session...") + session["github_oauth2_state"] = state + cla.log.debug(f"{fn} - GitHub OAuth2 request with state {state} - sending user to {authorization_url}") raise falcon.HTTPFound(authorization_url) def _get_request_session(self, request) -> dict: # pylint: disable=no-self-use """ Mockable method used to get the current user session. """ - fn = 'cla.models.github_models._get_request_session' - session = request.context.get('session') + fn = "cla.models.github_models._get_request_session" + session = request.context.get("session") if session is None: - cla.log.warning(f'Session is empty for request: {request}') - cla.log.debug(f'{fn} - loaded session: {session}') + cla.log.warning(f"Session is empty for request: {request}") + cla.log.debug(f"{fn} - loaded session: {session}") # Ensure session is a dict - getting issue where session is a string if isinstance(session, str): # convert string to a dict - cla.log.debug(f'{fn} - session is type: {type(session)} - converting to dict...') + cla.log.debug(f"{fn} - session is type: {type(session)} - converting to dict...") session = json.loads(session) # Reset the session now that we have converted it to a dict - request.context['session'] = session - cla.log.debug(f'{fn} - session: {session} which is now type: {type(session)}...') + request.context["session"] = session + cla.log.debug(f"{fn} - session: {session} which is now type: {type(session)}...") return session @@ -170,20 +174,21 @@ def get_authorization_url_and_state(self, installation_id, github_repository_id, # Get the PR's html_url property. # origin = self.get_return_url(github_repository_id, pull_request_number, installation_id) # Add origin to user's session here? - fn = 'github_models.get_authorization_url_and_state' - redirect_uri = os.environ.get('CLA_API_BASE', '').strip() + "/v2/github/installation" - github_oauth_url = cla.conf['GITHUB_OAUTH_AUTHORIZE_URL'] - github_oauth_client_id = os.environ['GH_OAUTH_CLIENT_ID'] - - cla.log.debug(f'{fn} - Directing user to the github authorization url: {github_oauth_url} via ' - f'our github installation flow: {redirect_uri} ' - f'using the github oauth client id: {github_oauth_client_id[0:5]} ' - f'with scope: {scope}') + fn = "github_models.get_authorization_url_and_state" + redirect_uri = os.environ.get("CLA_API_BASE", "").strip() + "/v2/github/installation" + github_oauth_url = cla.conf["GITHUB_OAUTH_AUTHORIZE_URL"] + github_oauth_client_id = os.environ["GH_OAUTH_CLIENT_ID"] + + cla.log.debug( + f"{fn} - Directing user to the github authorization url: {github_oauth_url} via " + f"our github installation flow: {redirect_uri} " + f"using the github oauth client id: {github_oauth_client_id[0:5]} " + f"with scope: {scope}" + ) - return self._get_authorization_url_and_state(client_id=github_oauth_client_id, - redirect_uri=redirect_uri, - scope=scope, - authorize_url=github_oauth_url) + return self._get_authorization_url_and_state( + client_id=github_oauth_client_id, redirect_uri=redirect_uri, scope=scope, authorize_url=github_oauth_url + ) def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url): """ @@ -199,48 +204,49 @@ def oauth2_redirect(self, state, code, request): # pylint: disable=too-many-arg It will handle storing the OAuth2 session information for this user for further requests and initiate the signing workflow. """ - fn = 'github_models.oauth2_redirect' - cla.log.debug(f'{fn} - handling GitHub OAuth2 redirect with request: {dir(request)}') + fn = "github_models.oauth2_redirect" + cla.log.debug(f"{fn} - handling GitHub OAuth2 redirect with request: {dir(request)}") session = self._get_request_session(request) # request.context['session'] - cla.log.debug(f'{fn} - state: {state}, code: {code}, session: {session}') + cla.log.debug(f"{fn} - state: {state}, code: {code}, session: {session}") - if 'github_oauth2_state' in session: - session_state = session['github_oauth2_state'] + if "github_oauth2_state" in session: + session_state = session["github_oauth2_state"] else: session_state = None - cla.log.warning(f'{fn} - github_oauth2_state not set in current session') + cla.log.warning(f"{fn} - github_oauth2_state not set in current session") if state != session_state: - cla.log.warning(f'{fn} - invalid GitHub OAuth2 state {session_state} expecting {state}') - raise falcon.HTTPBadRequest('Invalid OAuth2 state', state) + cla.log.warning(f"{fn} - invalid GitHub OAuth2 state {session_state} expecting {state}") + raise falcon.HTTPBadRequest("Invalid OAuth2 state", state) # Get session information for this request. - cla.log.debug(f'{fn} - attempting to fetch OAuth2 token for state {state}') - installation_id = session.get('github_installation_id', None) - github_repository_id = session.get('github_repository_id', None) - change_request_id = session.get('github_change_request_id', None) - origin_url = session.get('github_origin_url', None) - state = session.get('github_oauth2_state') - token_url = cla.conf['GITHUB_OAUTH_TOKEN_URL'] - client_id = os.environ['GH_OAUTH_CLIENT_ID'] - client_secret = os.environ['GH_OAUTH_SECRET'] - cla.log.debug(f'{fn} - fetching token using {client_id[0:5]}... with state={state}, token_url={token_url}, ' - f'client_secret={client_secret[0:5]}, with code={code}') + cla.log.debug(f"{fn} - attempting to fetch OAuth2 token for state {state}") + installation_id = session.get("github_installation_id", None) + github_repository_id = session.get("github_repository_id", None) + change_request_id = session.get("github_change_request_id", None) + origin_url = session.get("github_origin_url", None) + state = session.get("github_oauth2_state") + token_url = cla.conf["GITHUB_OAUTH_TOKEN_URL"] + client_id = os.environ["GH_OAUTH_CLIENT_ID"] + client_secret = os.environ["GH_OAUTH_SECRET"] + cla.log.debug( + f"{fn} - fetching token using {client_id[0:5]}... with state={state}, token_url={token_url}, " + f"client_secret={client_secret[0:5]}, with code={code}" + ) token = self._fetch_token(client_id, state, token_url, client_secret, code) - cla.log.debug(f'{fn} - oauth2 token received for state {state}: {token} - storing token in session') - session['github_oauth2_token'] = token - cla.log.debug(f'{fn} - redirecting the user back to the console: {origin_url}') + cla.log.debug(f"{fn} - oauth2 token received for state {state}: {token} - storing token in session") + session["github_oauth2_token"] = token + cla.log.debug(f"{fn} - redirecting the user back to the console: {origin_url}") return self.redirect_to_console(installation_id, github_repository_id, change_request_id, origin_url, request) def redirect_to_console(self, installation_id, repository_id, pull_request_id, origin_url, request): - fn = 'github_models.redirect_to_console' - console_endpoint = cla.conf['CONTRIBUTOR_BASE_URL'] - console_v2_endpoint = cla.conf['CONTRIBUTOR_V2_BASE_URL'] + fn = "github_models.redirect_to_console" + console_endpoint = cla.conf["CONTRIBUTOR_BASE_URL"] + console_v2_endpoint = cla.conf["CONTRIBUTOR_V2_BASE_URL"] # Get repository using github's repository ID. repository = Repository().get_repository_by_external_id(repository_id, "github") if repository is None: - cla.log.warning(f'{fn} - Could not find repository with the following ' - f'repository_id: {repository_id}') + cla.log.warning(f"{fn} - Could not find repository with the following " f"repository_id: {repository_id}") return None # Get project ID from this repository @@ -250,7 +256,7 @@ def redirect_to_console(self, installation_id, repository_id, pull_request_id, o project = get_project_instance() project.load(str(project_id)) except DoesNotExist as err: - return {'errors': {'project_id': str(err)}} + return {"errors": {"project_id": str(err)}} user = self.get_or_create_user(request) # Ensure user actually requires a signature for this project. @@ -270,28 +276,41 @@ def redirect_to_console(self, installation_id, repository_id, pull_request_id, o # Store repository and PR info so we can redirect the user back later. cla.utils.set_active_signature_metadata(user.get_user_id(), project_id, repository_id, pull_request_id) - console_url = '' + console_url = "" # Temporary condition until all CLA Groups are ready for the v2 Contributor Console - if project.get_version() == 'v2': + if project.get_version() == "v2": # Generate url for the v2 console - console_url = 'https://' + console_v2_endpoint + \ - '/#/cla/project/' + project_id + \ - '/user/' + user.get_user_id() + \ - '?redirect=' + origin_url - cla.log.debug(f'{fn} - redirecting to v2 console: {console_url}...') + console_url = ( + "https://" + + console_v2_endpoint + + "/#/cla/project/" + + project_id + + "/user/" + + user.get_user_id() + + "?redirect=" + + origin_url + ) + cla.log.debug(f"{fn} - redirecting to v2 console: {console_url}...") else: # Generate url for the v1 contributor console - console_url = 'https://' + console_endpoint + \ - '/#/cla/project/' + project_id + \ - '/user/' + user.get_user_id() + \ - '?redirect=' + origin_url - cla.log.debug(f'{fn} - redirecting to v1 console: {console_url}...') + console_url = ( + "https://" + + console_endpoint + + "/#/cla/project/" + + project_id + + "/user/" + + user.get_user_id() + + "?redirect=" + + origin_url + ) + cla.log.debug(f"{fn} - redirecting to v1 console: {console_url}...") raise falcon.HTTPFound(console_url) - def _fetch_token(self, client_id, state, token_url, client_secret, - code): # pylint: disable=too-many-arguments,no-self-use + def _fetch_token( + self, client_id, state, token_url, client_secret, code + ): # pylint: disable=too-many-arguments,no-self-use """ Mockable method to fetch a OAuth2Session token. """ @@ -302,16 +321,20 @@ def sign_workflow(self, installation_id, github_repository_id, pull_request_numb Once we have the 'github_oauth2_token' value in the user's session, we can initiate the signing workflow. """ - fn = 'sign_workflow' - cla.log.warning(f'{fn} - Initiating GitHub signing workflow for ' - f'GitHub repo {github_repository_id} ' - f'with PR: {pull_request_number}') + fn = "sign_workflow" + cla.log.warning( + f"{fn} - Initiating GitHub signing workflow for " + f"GitHub repo {github_repository_id} " + f"with PR: {pull_request_number}" + ) user = self.get_or_create_user(request) signature = cla.utils.get_user_signature_by_github_repository(installation_id, user) project_id = cla.utils.get_project_id_from_installation_id(installation_id) document = cla.utils.get_project_latest_individual_document(project_id) - if signature is not None and \ - signature.get_signature_document_major_version() == document.get_document_major_version(): + if ( + signature is not None + and signature.get_signature_document_major_version() == document.get_document_major_version() + ): return cla.utils.redirect_user_by_signature(user, signature) else: # Signature not found or older version, create new one and send user to sign. @@ -324,11 +347,11 @@ def process_opened_pull_request(self, data): :param data: The data returned from GitHub on this webhook. :type data: dict """ - pull_request_id = data['pull_request']['number'] - github_repository_id = data['repository']['id'] - installation_id = data['installation']['id'] + pull_request_id = data["pull_request"]["number"] + github_repository_id = data["repository"]["id"] + installation_id = data["installation"]["id"] self.update_change_request(installation_id, github_repository_id, pull_request_id) - + def process_checks_requested_merge_group(self, data): """ Helper method to handle a webhook fired from GitHub for a merge group event. @@ -336,10 +359,10 @@ def process_checks_requested_merge_group(self, data): :param data: The data returned from GitHub on this webhook. :type data: dict """ - merge_group_sha = data['merge_group']['head_sha'] - github_repository_id = data['repository']['id'] - installation_id = data['installation']['id'] - pull_request_message = data['merge_group']['head_commit']['message'] + merge_group_sha = data["merge_group"]["head_sha"] + github_repository_id = data["repository"]["id"] + installation_id = data["installation"]["id"] + pull_request_message = data["merge_group"]["head_commit"]["message"] # Extract the pull request number from the message pull_request_id = cla.utils.extract_pull_request_number(pull_request_message) @@ -356,10 +379,11 @@ def process_easycla_command_comment(self, data): raise ValueError("missing comment body, ignoring the message") if "/easycla" not in comment_str.split(): - raise ValueError(f'unsupported comment supplied: {comment_str.split()}, ' - 'currently only the \'/easycla\' command is supported') + raise ValueError( + f"unsupported comment supplied: {comment_str.split()}, " "currently only the '/easycla' command is supported" + ) - github_repository_id = data.get('repository', {}).get('id', None) + github_repository_id = data.get("repository", {}).get("id", None) if not github_repository_id: raise ValueError("missing github repository id in pull request comment") cla.log.debug(f"comment trigger for github_repo : {github_repository_id}") @@ -370,8 +394,8 @@ def process_easycla_command_comment(self, data): raise ValueError("missing pull request id ") cla.log.debug(f"comment trigger for pull_request_id : {pull_request_id}") - cla.log.debug("installation object : ", data.get('installation', {})) - installation_id = data.get('installation', {}).get('id', None) + cla.log.debug("installation object : ", data.get("installation", {})) + installation_id = data.get("installation", {}).get("id", None) if not installation_id: raise ValueError("missing installation id in pull request comment") cla.log.debug(f"comment trigger for installation_id : {installation_id}") @@ -381,53 +405,54 @@ def process_easycla_command_comment(self, data): def get_return_url(self, github_repository_id, change_request_id, installation_id): pull_request = self.get_pull_request(github_repository_id, change_request_id, installation_id) return pull_request.html_url - + def get_existing_repository(self, github_repository_id): - fn = 'get_existing_repository' + fn = "get_existing_repository" # Queries GH for the complete repository details, see: # https://developer.github.com/v3/repos/#get-a-repository - cla.log.debug(f'{fn} - fetching repository details for GH repo ID: {github_repository_id}...') - repository = Repository().get_repository_by_external_id(str(github_repository_id), 'github') + cla.log.debug(f"{fn} - fetching repository details for GH repo ID: {github_repository_id}...") + repository = Repository().get_repository_by_external_id(str(github_repository_id), "github") if repository is None: - cla.log.warning(f'{fn} - unable to locate repository by GH ID: {github_repository_id}') + cla.log.warning(f"{fn} - unable to locate repository by GH ID: {github_repository_id}") return None - + if repository.get_enabled() is False: - cla.log.warning(f'{fn} - repository is disabled, skipping: {github_repository_id}') + cla.log.warning(f"{fn} - repository is disabled, skipping: {github_repository_id}") return None - - cla.log.debug(f'{fn} - found repository by GH ID: {github_repository_id}') + + cla.log.debug(f"{fn} - found repository by GH ID: {github_repository_id}") return repository - - + def check_org_validity(self, installation_id, repository): - fn = 'check_org_validity' + fn = "check_org_validity" organization_name = repository.get_organization_name() # Check that the Github Organization exists in our database - cla.log.debug(f'{fn} - fetching organization details for GH org name: {organization_name}...') + cla.log.debug(f"{fn} - fetching organization details for GH org name: {organization_name}...") github_org = GitHubOrg() try: github_org.load(organization_name=organization_name) except DoesNotExist as err: - cla.log.warning(f'{fn} - unable to locate organization by GH name: {organization_name}') + cla.log.warning(f"{fn} - unable to locate organization by GH name: {organization_name}") return False - + if github_org.get_organization_installation_id() != installation_id: - cla.log.warning(f'{fn} - ' - f'the installation ID: {github_org.get_organization_installation_id()} ' - f'of this organization does not match installation ID: {installation_id} ' - 'given by the pull request.') + cla.log.warning( + f"{fn} - " + f"the installation ID: {github_org.get_organization_installation_id()} " + f"of this organization does not match installation ID: {installation_id} " + "given by the pull request." + ) return False - cla.log.debug(f'{fn} - found organization by GH name: {organization_name}') + cla.log.debug(f"{fn} - found organization by GH name: {organization_name}") return True - + def get_pull_request_retry(self, github_repository_id, change_request_id, installation_id, retries=3) -> dict: """ Helper function to retry getting a pull request from GitHub. """ - fn = 'get_pull_request_retry' + fn = "get_pull_request_retry" pull_request = {} for i in range(retries): try: @@ -435,26 +460,32 @@ def get_pull_request_retry(self, github_repository_id, change_request_id, instal _ = int(change_request_id) pull_request = self.get_pull_request(github_repository_id, change_request_id, installation_id) except ValueError as ve: - cla.log.error(f'{fn} - Invalid PR: {change_request_id} - error: {ve}. Unable to fetch ' - f'PR {change_request_id} from GitHub repository {github_repository_id} ' - f'using installation id {installation_id}.') + cla.log.error( + f"{fn} - Invalid PR: {change_request_id} - error: {ve}. Unable to fetch " + f"PR {change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) if i <= retries: - cla.log.debug(f'{fn} - attempt {i + 1} - waiting to retry...') + cla.log.debug(f"{fn} - attempt {i + 1} - waiting to retry...") time.sleep(2) continue else: - cla.log.warning(f'{fn} - attempt {i + 1} - exhausted retries - unable to load PR ' - f'{change_request_id} from GitHub repository {github_repository_id} ' - f'using installation id {installation_id}.') + cla.log.warning( + f"{fn} - attempt {i + 1} - exhausted retries - unable to load PR " + f"{change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) # TODO: DAD - possibly update the PR status? return # Fell through - no error, exit loop and continue on break - cla.log.debug(f'{fn} - retrieved pull request: {pull_request}') + cla.log.debug(f"{fn} - retrieved pull request: {pull_request}") return pull_request - def update_merge_group_status(self,installation_id, repository_id, pull_request,merge_commit_sha,signed, missing, project_version): + def update_merge_group_status( + self, installation_id, repository_id, pull_request, merge_commit_sha, signed, missing, project_version + ): """ Helper function to update a merge queue entrys status based on the list of signers. :param installation_id: The ID of the GitHub installation @@ -463,61 +494,73 @@ def update_merge_group_status(self,installation_id, repository_id, pull_request, :type repository_id: int :param pull_request: The GitHub PullRequest object for this PR. """ - fn = 'update_merge_group_status' - context_name = os.environ.get('GH_STATUS_CTX_NAME') + fn = "update_merge_group_status" + context_name = os.environ.get("GH_STATUS_CTX_NAME") if context_name is None: - context_name = 'communitybridge/cla' + context_name = "communitybridge/cla" if missing is not None and len(missing) > 0: - state = 'failure' + state = "failure" context, body = cla.utils.assemble_cla_status(context_name, signed=False) sign_url = cla.utils.get_full_sign_url( - 'github', str(installation_id), repository_id, pull_request.number, project_version) - cla.log.debug(f'{fn} - Creating new CLA \'{state}\' status - {len(signed)} passed, {missing} failed, ' - f'signing url: {sign_url}') + "github", str(installation_id), repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) elif signed is not None and len(signed) > 0: - state = 'success' + state = "success" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=True) sign_url = cla.conf["CLA_LANDING_PAGE"] # Remove this once signature detail page ready. sign_url = os.path.join(sign_url, "#/") sign_url = append_project_version_to_url(address=sign_url, project_version=project_version) - cla.log.debug(f'{fn} - Creating new CLA \'{state}\' status - {len(signed)} passed, {missing} failed, ' - f'signing url: {sign_url}') + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) else: # error condition - should have at least one committer, and they would be in one of the above # lists: missing or signed - state = 'failure' + state = "failure" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=False) sign_url = cla.utils.get_full_sign_url( - 'github', str(installation_id), repository_id, pull_request.number, project_version) - cla.log.debug(f'{fn} - Creating new CLA \'{state}\' status - {len(signed)} passed, {missing} failed, ' - f'signing url: {sign_url}') - cla.log.warning('{fn} - This is an error condition - ' - f'should have at least one committer in one of these lists: ' - f'{len(signed)} passed, {missing}') - + "github", str(installation_id), repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) + cla.log.warning( + "{fn} - This is an error condition - " + f"should have at least one committer in one of these lists: " + f"{len(signed)} passed, {missing}" + ) + # Create the commit status on the merge commit if self.client is None: self.client = self._get_github_client(installation_id) - + # Get repository - cla.log.debug(f'{fn} - Getting repository by ID: {repository_id}') + cla.log.debug(f"{fn} - Getting repository by ID: {repository_id}") repository = self.client.get_repo(int(repository_id)) # Get the commit object - cla.log.debug(f'{fn} - Getting commit by SHA: {merge_commit_sha}') + cla.log.debug(f"{fn} - Getting commit by SHA: {merge_commit_sha}") commit_obj = repository.get_commit(merge_commit_sha) - cla.log.debug(f'{fn} - Creating commit status for merge commit: {merge_commit_sha} ' - f'with state: {state}, context: {context}, body: {body}') + cla.log.debug( + f"{fn} - Creating commit status for merge commit: {merge_commit_sha} " + f"with state: {state}, context: {context}, body: {body}" + ) - create_commit_status_for_merge_group(commit_obj,merge_commit_sha, state, sign_url, body, context) + create_commit_status_for_merge_group(commit_obj, merge_commit_sha, state, sign_url, body, context) def update_merge_group(self, installation_id, github_repository_id, merge_group_sha, pull_request_id): - fn = 'update_queue_entry' + fn = "update_queue_entry" # Note: late 2021/early 2022 we observed that sometimes we get the event for a PR, then go back to GitHub # to query for the PR details and discover the PR is 404, not available for some reason. Added retry @@ -525,33 +568,41 @@ def update_merge_group(self, installation_id, github_repository_id, merge_group_ try: # Get the pull request details from GitHub - cla.log.debug(f'{fn} - fetching pull request details for GH repo ID: {github_repository_id} ' - f'PR ID: {pull_request_id}...') + cla.log.debug( + f"{fn} - fetching pull request details for GH repo ID: {github_repository_id} " + f"PR ID: {pull_request_id}..." + ) pull_request = self.get_pull_request_retry(github_repository_id, pull_request_id, installation_id) except Exception as e: - cla.log.warning(f'{fn} - unable to load PR {pull_request_id} from GitHub repository ' - f'{github_repository_id} using installation id {installation_id} - error: {e}') + cla.log.warning( + f"{fn} - unable to load PR {pull_request_id} from GitHub repository " + f"{github_repository_id} using installation id {installation_id} - error: {e}" + ) return - try : + try: # Get Commit authors - commit_authors = get_pull_request_commit_authors(pull_request,installation_id) - cla.log.debug(f'{fn} - commit authors: {commit_authors}') + commit_authors = get_pull_request_commit_authors(pull_request, installation_id) + cla.log.debug(f"{fn} - commit authors: {commit_authors}") except Exception as e: - cla.log.warning(f'{fn} - unable to load commit authors for PR {pull_request_id} from GitHub repository ' - f'{github_repository_id} using installation id {installation_id} - error: {e}') + cla.log.warning( + f"{fn} - unable to load commit authors for PR {pull_request_id} from GitHub repository " + f"{github_repository_id} using installation id {installation_id} - error: {e}" + ) return - + try: # Get existing repository info using the repository's external ID, # which is the repository ID assigned by github. - cla.log.debug(f'{fn} - PR: {pull_request.number}, Loading GitHub repository by id: {github_repository_id}') + cla.log.debug(f"{fn} - PR: {pull_request.number}, Loading GitHub repository by id: {github_repository_id}") repository = Repository().get_repository_by_external_id(github_repository_id, "github") if repository is None: - cla.log.warning(f'{fn} - PR: {pull_request.number}, Failed to load GitHub repository by ' - f'id: {github_repository_id} in our DB - repository reference is None - ' - 'Is this org/repo configured in the Project Console?' - ' Unable to update status.') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Failed to load GitHub repository by " + f"id: {github_repository_id} in our DB - repository reference is None - " + "Is this org/repo configured in the Project Console?" + " Unable to update status." + ) # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA # app/bot is enabled in GitHub (which is why we received the event in the first place), but the # repository is not setup/configured in EasyCLA from the administration console @@ -559,46 +610,60 @@ def update_merge_group(self, installation_id, github_repository_id, merge_group_ # If the repository is not enabled in our database, we don't process it. if not repository.get_enabled(): - cla.log.warning(f'{fn} - repository {repository.get_repository_url()} associated with ' - f'PR: {pull_request.number} is NOT enabled' - ' - ignoring PR request') + cla.log.warning( + f"{fn} - repository {repository.get_repository_url()} associated with " + f"PR: {pull_request.number} is NOT enabled" + " - ignoring PR request" + ) # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA # app/bot is enabled in GitHub (which is why we received the event in the first place), but the # repository is NOT enabled in the administration console return except DoesNotExist: - cla.log.warning(f'{fn} - PR: {pull_request.number}, could not find repository with the ' - f'repository ID: {github_repository_id}') - cla.log.warning(f'{fn} - PR: {pull_request.number}, failed to update change request of ' - f'repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, could not find repository with the " + f"repository ID: {github_repository_id}" + ) + cla.log.warning( + f"{fn} - PR: {pull_request.number}, failed to update change request of " + f"repository {github_repository_id} - returning" + ) return # Get GitHub Organization name that the repository is configured to. organization_name = repository.get_repository_organization_name() - cla.log.debug(f'{fn} - PR: {pull_request.number}, determined github organization is: {organization_name}') + cla.log.debug(f"{fn} - PR: {pull_request.number}, determined github organization is: {organization_name}") # Check that the GitHub Organization exists. github_org = GitHubOrg() try: github_org.load(organization_name) except DoesNotExist: - cla.log.warning(f'{fn} - PR: {pull_request.number}, Could not find Github Organization ' - f'with the following organization name: {organization_name}') - cla.log.warning(f'{fn}- PR: {pull_request.number}, Failed to update change request of ' - f'repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Could not find Github Organization " + f"with the following organization name: {organization_name}" + ) + cla.log.warning( + f"{fn}- PR: {pull_request.number}, Failed to update change request of " + f"repository {github_repository_id} - returning" + ) return # Ensure that installation ID for this organization matches the given installation ID if github_org.get_organization_installation_id() != installation_id: - cla.log.warning(f'{fn} - PR: {pull_request.number}, ' - f'the installation ID: {github_org.get_organization_installation_id()} ' - f'of this organization does not match installation ID: {installation_id} ' - 'given by the pull request.') - cla.log.error(f'{fn} - PR: {pull_request.number}, Failed to update change request ' - f'of repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, " + f"the installation ID: {github_org.get_organization_installation_id()} " + f"of this organization does not match installation ID: {installation_id} " + "given by the pull request." + ) + cla.log.error( + f"{fn} - PR: {pull_request.number}, Failed to update change request " + f"of repository {github_repository_id} - returning" + ) return - + project_id = repository.get_repository_project_id() project = get_project_instance() project.load(project_id) @@ -607,17 +672,17 @@ def update_merge_group(self, installation_id, github_repository_id, merge_group_ missing = [] # Check if the user has signed the CLA - cla.log.debug(f'{fn} - checking if the user has signed the CLA...') + cla.log.debug(f"{fn} - checking if the user has signed the CLA...") for user_commit_summary in commit_authors: handle_commit_from_user(project, user_commit_summary, signed, missing) - - #update Merge group status - self.update_merge_group_status(installation_id, github_repository_id, pull_request, merge_group_sha, signed, missing, project.get_version()) + # update Merge group status + self.update_merge_group_status( + installation_id, github_repository_id, pull_request, merge_group_sha, signed, missing, project.get_version() + ) - def update_change_request(self, installation_id, github_repository_id, change_request_id): - fn = 'update_change_request' + fn = "update_change_request" # Queries GH for the complete pull request details, see: # https://developer.github.com/v3/pulls/#response-1 @@ -632,39 +697,47 @@ def update_change_request(self, installation_id, github_repository_id, change_re _ = int(change_request_id) pull_request = self.get_pull_request(github_repository_id, change_request_id, installation_id) except ValueError as ve: - cla.log.error(f'{fn} - Invalid PR: {change_request_id} - error: {ve}. Unable to fetch ' - f'PR {change_request_id} from GitHub repository {github_repository_id} ' - f'using installation id {installation_id}.') + cla.log.error( + f"{fn} - Invalid PR: {change_request_id} - error: {ve}. Unable to fetch " + f"PR {change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) if i <= tries: - cla.log.debug(f'{fn} - attempt {i + 1} - waiting to retry...') + cla.log.debug(f"{fn} - attempt {i + 1} - waiting to retry...") time.sleep(2) continue else: - cla.log.warning(f'{fn} - attempt {i + 1} - exhausted retries - unable to load PR ' - f'{change_request_id} from GitHub repository {github_repository_id} ' - f'using installation id {installation_id}.') + cla.log.warning( + f"{fn} - attempt {i + 1} - exhausted retries - unable to load PR " + f"{change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) # TODO: DAD - possibly update the PR status? return # Fell through - no error, exit loop and continue on break - cla.log.debug(f'{fn} - retrieved pull request: {pull_request}') + cla.log.debug(f"{fn} - retrieved pull request: {pull_request}") # Get all unique users/authors involved in this PR - returns a List[UserCommitSummary] objects - commit_authors = get_pull_request_commit_authors(pull_request,installation_id) + commit_authors = get_pull_request_commit_authors(pull_request, installation_id) - cla.log.debug(f'{fn} - PR: {pull_request.number}, found {len(commit_authors)} unique commit authors ' - f'for pull request: {pull_request.number}') + cla.log.debug( + f"{fn} - PR: {pull_request.number}, found {len(commit_authors)} unique commit authors " + f"for pull request: {pull_request.number}" + ) try: # Get existing repository info using the repository's external ID, # which is the repository ID assigned by github. - cla.log.debug(f'{fn} - PR: {pull_request.number}, Loading GitHub repository by id: {github_repository_id}') + cla.log.debug(f"{fn} - PR: {pull_request.number}, Loading GitHub repository by id: {github_repository_id}") repository = Repository().get_repository_by_external_id(github_repository_id, "github") if repository is None: - cla.log.warning(f'{fn} - PR: {pull_request.number}, Failed to load GitHub repository by ' - f'id: {github_repository_id} in our DB - repository reference is None - ' - 'Is this org/repo configured in the Project Console?' - ' Unable to update status.') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Failed to load GitHub repository by " + f"id: {github_repository_id} in our DB - repository reference is None - " + "Is this org/repo configured in the Project Console?" + " Unable to update status." + ) # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA # app/bot is enabled in GitHub (which is why we received the event in the first place), but the # repository is not setup/configured in EasyCLA from the administration console @@ -672,44 +745,58 @@ def update_change_request(self, installation_id, github_repository_id, change_re # If the repository is not enabled in our database, we don't process it. if not repository.get_enabled(): - cla.log.warning(f'{fn} - repository {repository.get_repository_url()} associated with ' - f'PR: {pull_request.number} is NOT enabled' - ' - ignoring PR request') + cla.log.warning( + f"{fn} - repository {repository.get_repository_url()} associated with " + f"PR: {pull_request.number} is NOT enabled" + " - ignoring PR request" + ) # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA # app/bot is enabled in GitHub (which is why we received the event in the first place), but the # repository is NOT enabled in the administration console return except DoesNotExist: - cla.log.warning(f'{fn} - PR: {pull_request.number}, could not find repository with the ' - f'repository ID: {github_repository_id}') - cla.log.warning(f'{fn} - PR: {pull_request.number}, failed to update change request of ' - f'repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, could not find repository with the " + f"repository ID: {github_repository_id}" + ) + cla.log.warning( + f"{fn} - PR: {pull_request.number}, failed to update change request of " + f"repository {github_repository_id} - returning" + ) return # Get GitHub Organization name that the repository is configured to. organization_name = repository.get_repository_organization_name() - cla.log.debug(f'{fn} - PR: {pull_request.number}, determined github organization is: {organization_name}') + cla.log.debug(f"{fn} - PR: {pull_request.number}, determined github organization is: {organization_name}") # Check that the GitHub Organization exists. github_org = GitHubOrg() try: github_org.load(organization_name) except DoesNotExist: - cla.log.warning(f'{fn} - PR: {pull_request.number}, Could not find Github Organization ' - f'with the following organization name: {organization_name}') - cla.log.warning(f'{fn}- PR: {pull_request.number}, Failed to update change request of ' - f'repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Could not find Github Organization " + f"with the following organization name: {organization_name}" + ) + cla.log.warning( + f"{fn}- PR: {pull_request.number}, Failed to update change request of " + f"repository {github_repository_id} - returning" + ) return # Ensure that installation ID for this organization matches the given installation ID if github_org.get_organization_installation_id() != installation_id: - cla.log.warning(f'{fn} - PR: {pull_request.number}, ' - f'the installation ID: {github_org.get_organization_installation_id()} ' - f'of this organization does not match installation ID: {installation_id} ' - 'given by the pull request.') - cla.log.error(f'{fn} - PR: {pull_request.number}, Failed to update change request ' - f'of repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, " + f"the installation ID: {github_org.get_organization_installation_id()} " + f"of this organization does not match installation ID: {installation_id} " + "given by the pull request." + ) + cla.log.error( + f"{fn} - PR: {pull_request.number}, Failed to update change request " + f"of repository {github_repository_id} - returning" + ) return # Retrieve project ID from the repository. @@ -727,33 +814,36 @@ def update_change_request(self, installation_id, github_repository_id, change_re pull_request_id=str(change_request_id), ) except Exception as e: - cla.log.error(f'{fn} - problem saving PR metadata for PR: {pull_request.number}, error: {e}') + cla.log.error(f"{fn} - problem saving PR metadata for PR: {pull_request.number}, error: {e}") # Find users who have signed and who have not signed. signed = [] missing = [] futures = [] - cla.log.debug(f'{fn} - PR: {pull_request.number}, scanning users - ' - 'determining who has signed a CLA an who has not.') - + cla.log.debug( + f"{fn} - PR: {pull_request.number}, scanning users - " "determining who has signed a CLA an who has not." + ) + with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: for user_commit_summary in commit_authors: - cla.log.debug(f'{fn} - PR: {pull_request.number} for user: {user_commit_summary}') - futures.append(executor.submit(handle_commit_from_user, project,user_commit_summary,signed,missing)) + cla.log.debug(f"{fn} - PR: {pull_request.number} for user: {user_commit_summary}") + futures.append(executor.submit(handle_commit_from_user, project, user_commit_summary, signed, missing)) - #Wait for all threads to be finished before moving on + # Wait for all threads to be finished before moving on executor.shutdown(wait=True) for future in concurrent.futures.as_completed(futures): - cla.log.debug(f'{fn} - ThreadClosed for handle_commit_from_user') + cla.log.debug(f"{fn} - ThreadClosed for handle_commit_from_user") # At this point, the signed and missing lists are now filled and updated with the commit user info - cla.log.debug(f'{fn} - PR: {pull_request.number}, ' - f'updating github pull request for repo: {github_repository_id}, ' - f'with signed authors: {signed} ' - f'with missing authors: {missing}') + cla.log.debug( + f"{fn} - PR: {pull_request.number}, " + f"updating github pull request for repo: {github_repository_id}, " + f"with signed authors: {signed} " + f"with missing authors: {missing}" + ) repository_name = repository.get_repository_name() update_pull_request( installation_id=installation_id, @@ -762,7 +852,8 @@ def update_change_request(self, installation_id, github_repository_id, change_re repository_name=repository_name, signed=signed, missing=missing, - project_version=project.get_version()) + project_version=project.get_version(), + ) def get_pull_request(self, github_repository_id, pull_request_number, installation_id): """ @@ -775,19 +866,22 @@ def get_pull_request(self, github_repository_id, pull_request_number, installati :param installation_id: The ID of the GitHub application installed on this repository. :type installation_id: int | None """ - cla.log.debug('Getting PR %s from GitHub repository %s', pull_request_number, github_repository_id) + cla.log.debug("Getting PR %s from GitHub repository %s", pull_request_number, github_repository_id) if self.client is None: self.client = get_github_integration_client(installation_id) repo = self.client.get_repo(int(github_repository_id)) try: return repo.get_pull(int(pull_request_number)) except UnknownObjectException: - cla.log.error('Could not find pull request %s for repository %s - ensure it ' - 'exists and that your personal access token has the "repo" scope enabled', - pull_request_number, github_repository_id) + cla.log.error( + "Could not find pull request %s for repository %s - ensure it " + 'exists and that your personal access token has the "repo" scope enabled', + pull_request_number, + github_repository_id, + ) except BadCredentialsException as err: - cla.log.error('Invalid GitHub credentials provided: %s', str(err)) - + cla.log.error("Invalid GitHub credentials provided: %s", str(err)) + def get_github_user_by_email(self, email, installation_id): """ Helper method to get the GitHub user object from GitHub. @@ -799,21 +893,20 @@ def get_github_user_by_email(self, email, installation_id): :param installation_id: The ID of the GitHub application installed on this repository. :type installation_id: int | None """ - cla.log.debug('Getting GitHub user %s', email) + cla.log.debug("Getting GitHub user %s", email) if self.client is None: self.client = get_github_integration_client(installation_id) try: - cla.log.debug('Searching for GitHub user by email handle: %s', email) + cla.log.debug("Searching for GitHub user by email handle: %s", email) users_by_email = self.client.search_users(f"{email} in:email") if len(list(users_by_email)) == 0: - cla.log.debug('No GitHub user found with email handle: %s', email) + cla.log.debug("No GitHub user found with email handle: %s", email) return None return list(users_by_email)[0] except UnknownObjectException: - cla.log.error('Could not find GitHub user %s' , - email) + cla.log.error("Could not find GitHub user %s", email) except BadCredentialsException as err: - cla.log.error('Invalid GitHub credentials provided: %s', str(err)) + cla.log.error("Invalid GitHub credentials provided: %s", str(err)) def get_or_create_user(self, request): """ @@ -822,36 +915,40 @@ def get_or_create_user(self, request): :param request: The hug request object for this API call. :type request: Request """ - fn = 'github_models.get_or_create_user' + fn = "github_models.get_or_create_user" session = self._get_request_session(request) - github_user = self.get_user_data(session, os.environ['GH_OAUTH_CLIENT_ID']) - if 'error' in github_user: + github_user = self.get_user_data(session, os.environ["GH_OAUTH_CLIENT_ID"]) + if "error" in github_user: # Could not get GitHub user data - maybe user revoked CLA app permissions? session = self._get_request_session(request) - del session['github_oauth2_state'] - del session['github_oauth2_token'] - cla.log.warning(f'{fn} - Deleted OAuth2 session data - retrying token exchange next time') - raise falcon.HTTPError('400 Bad Request', 'github_oauth2_token', - 'Token permissions have been rejected, please try again') + del session["github_oauth2_state"] + del session["github_oauth2_token"] + cla.log.warning(f"{fn} - Deleted OAuth2 session data - retrying token exchange next time") + raise falcon.HTTPError( + "400 Bad Request", "github_oauth2_token", "Token permissions have been rejected, please try again" + ) - emails = self.get_user_emails(session, os.environ['GH_OAUTH_CLIENT_ID']) + emails = self.get_user_emails(session, os.environ["GH_OAUTH_CLIENT_ID"]) if len(emails) < 1: - cla.log.warning(f'{fn} - GitHub user has no verified email address: %s (%s)', - github_user['name'], github_user['login']) + cla.log.warning( + f"{fn} - GitHub user has no verified email address: %s (%s)", github_user["name"], github_user["login"] + ) raise falcon.HTTPError( - '412 Precondition Failed', 'email', - 'Please verify at least one email address with GitHub') + "412 Precondition Failed", "email", "Please verify at least one email address with GitHub" + ) - cla.log.debug(f'{fn} - Trying to load GitHub user by GitHub ID: %s', github_user['id']) - users = cla.utils.get_user_instance().get_user_by_github_id(github_user['id']) + cla.log.debug(f"{fn} - Trying to load GitHub user by GitHub ID: %s", github_user["id"]) + users = cla.utils.get_user_instance().get_user_by_github_id(github_user["id"]) if users is not None: # Users search can return more than one match - so it's an array - we set the first record value for now?? user = users[0] - cla.log.debug(f'{fn} - Loaded GitHub user by GitHub ID: %s - %s (%s)', - user.get_user_name(), - user.get_user_emails(), - user.get_user_github_id()) + cla.log.debug( + f"{fn} - Loaded GitHub user by GitHub ID: %s - %s (%s)", + user.get_user_name(), + user.get_user_emails(), + user.get_user_github_id(), + ) # update/set the github username if available cla.utils.update_github_username(github_user, user) @@ -861,7 +958,7 @@ def get_or_create_user(self, request): return user # User not found by GitHub ID, trying by email. - cla.log.debug(f'{fn} - Could not find GitHub user by GitHub ID: %s', github_user['id']) + cla.log.debug(f"{fn} - Could not find GitHub user by GitHub ID: %s", github_user["id"]) # TODO: This is very slow and needs to be improved - may need a DB schema change. users = None user = cla.utils.get_user_instance() @@ -874,27 +971,29 @@ def get_or_create_user(self, request): # Users search can return more than one match - so it's an array - we set the first record value for now?? user = users[0] # Found user by email, setting the GitHub ID - user.set_user_github_id(github_user['id']) + user.set_user_github_id(github_user["id"]) # update/set the github username if available cla.utils.update_github_username(github_user, user) user.set_user_emails(emails) user.save() - cla.log.debug(f'{fn} - Loaded GitHub user by email: {user}') + cla.log.debug(f"{fn} - Loaded GitHub user by email: {user}") return user # User not found, create. - cla.log.debug(f'{fn} - Could not find GitHub user by email: {emails}') - cla.log.debug(f'{fn} - Creating new GitHub user {github_user["name"]} - ' - f'({github_user["id"]}/{github_user["login"]}), ' - f'emails: {emails}') + cla.log.debug(f"{fn} - Could not find GitHub user by email: {emails}") + cla.log.debug( + f'{fn} - Creating new GitHub user {github_user["name"]} - ' + f'({github_user["id"]}/{github_user["login"]}), ' + f"emails: {emails}" + ) user = cla.utils.get_user_instance() user.set_user_id(str(uuid.uuid4())) user.set_user_emails(emails) - user.set_user_name(github_user['name']) - user.set_user_github_id(github_user['id']) - user.set_user_github_username(github_user['login']) + user.set_user_name(github_user["name"]) + user.set_user_github_id(github_user["id"]) + user.set_user_github_username(github_user["login"]) user.save() return user @@ -908,19 +1007,19 @@ def get_user_data(self, session, client_id): # pylint: disable=no-self-use :param client_id: The GitHub OAuth2 client ID. :type client_id: string """ - fn = 'cla.models.github_models.get_user_data' - token = session.get('github_oauth2_token') + fn = "cla.models.github_models.get_user_data" + token = session.get("github_oauth2_token") if token is None: - cla.log.error(f'{fn} - unable to load github_oauth2_token from session, session is: {session}') - return {'error': 'could not get user data from session'} + cla.log.error(f"{fn} - unable to load github_oauth2_token from session, session is: {session}") + return {"error": "could not get user data from session"} oauth2 = OAuth2Session(client_id, token=token) - request = oauth2.get('https://api.github.com/user') + request = oauth2.get("https://api.github.com/user") github_user = request.json() - cla.log.debug(f'{fn} - GitHub user data: %s', github_user) - if 'message' in github_user: + cla.log.debug(f"{fn} - GitHub user data: %s", github_user) + if "message" in github_user: cla.log.error(f'{fn} - Could not get user data with OAuth2 token: {github_user["message"]}') - return {'error': 'Could not get user data: %s' % github_user['message']} + return {"error": "Could not get user data: %s" % github_user["message"]} return github_user def get_user_emails(self, session: dict, client_id: str) -> Union[List[str], dict]: # pylint: disable=no-self-use @@ -933,15 +1032,13 @@ def get_user_emails(self, session: dict, client_id: str) -> Union[List[str], dic :type client_id: string """ emails = self._fetch_github_emails(session=session, client_id=client_id) - cla.log.debug('GitHub user emails: %s', emails) - if 'error' in emails: + cla.log.debug("GitHub user emails: %s", emails) + if "error" in emails: return emails - verified_emails = [item['email'] for item in emails if item['verified']] - excluded_emails = [email for email in verified_emails - if any([email.endswith(e) for e in EXCLUDE_GITHUB_EMAILS])] - included_emails = [email for email in verified_emails - if not any([email.endswith(e) for e in EXCLUDE_GITHUB_EMAILS])] + verified_emails = [item["email"] for item in emails if item["verified"]] + excluded_emails = [email for email in verified_emails if any([email.endswith(e) for e in EXCLUDE_GITHUB_EMAILS])] + included_emails = [email for email in verified_emails if not any([email.endswith(e) for e in EXCLUDE_GITHUB_EMAILS])] if len(included_emails) > 0: return included_emails @@ -953,11 +1050,11 @@ def get_primary_user_email(self, request) -> Union[Optional[str], dict]: """ gets the user primary email from the registered emails from the github api """ - fn = 'github_models.get_primary_user_email' + fn = "github_models.get_primary_user_email" try: - cla.log.debug(f'{fn} - fetching Github primary email') + cla.log.debug(f"{fn} - fetching Github primary email") session = self._get_request_session(request) - client_id = os.environ['GH_OAUTH_CLIENT_ID'] + client_id = os.environ["GH_OAUTH_CLIENT_ID"] emails = self._fetch_github_emails(session=session, client_id=client_id) if "error" in emails: return None @@ -966,7 +1063,7 @@ def get_primary_user_email(self, request) -> Union[Optional[str], dict]: if email.get("verified", False) and email.get("primary", False): return email["email"] except Exception as e: - cla.log.warning(f'{fn} - lookup failed - {e} - returning None') + cla.log.warning(f"{fn} - lookup failed - {e} - returning None") return None return None @@ -977,18 +1074,18 @@ def _fetch_github_emails(self, session: dict, client_id: str) -> Union[List[dict :param client_id: :return: """ - fn = 'github_models._fetch_github_emails' # function name + fn = "github_models._fetch_github_emails" # function name # Use the user's token to fetch their public email(s) - don't use the system token as this endpoint won't work # as expected - token = session.get('github_oauth2_token') + token = session.get("github_oauth2_token") if token is None: - cla.log.warning(f'{fn} - unable to load github_oauth2_token from the session - session is empty') + cla.log.warning(f"{fn} - unable to load github_oauth2_token from the session - session is empty") oauth2 = OAuth2Session(client_id, token=token) - request = oauth2.get('https://api.github.com/user/emails') + request = oauth2.get("https://api.github.com/user/emails") resp = request.json() - if 'message' in resp: + if "message" in resp: cla.log.warning(f'{fn} - could not get user emails with OAuth2 token: {resp["message"]}') - return {'error': 'Could not get user emails: %s' % resp['message']} + return {"error": "Could not get user emails: %s" % resp["message"]} return resp def process_reopened_pull_request(self, data): @@ -1038,22 +1135,23 @@ def create_repository(data): repository = cla.utils.get_repository_instance() repository.set_repository_id(str(uuid.uuid4())) # TODO: Need to use an ID unique across all repository providers instead of namespace. - full_name = data['repository']['full_name'] - namespace = full_name.split('/')[0] + full_name = data["repository"]["full_name"] + namespace = full_name.split("/")[0] repository.set_repository_project_id(namespace) - repository.set_repository_external_id(data['repository']['id']) + repository.set_repository_external_id(data["repository"]["id"]) repository.set_repository_name(full_name) - repository.set_repository_type('github') - repository.set_repository_url(data['repository']['html_url']) + repository.set_repository_type("github") + repository.set_repository_url(data["repository"]["html_url"]) repository.save() return repository except Exception as err: - cla.log.warning('Could not create GitHub repository automatically: %s', str(err)) + cla.log.warning("Could not create GitHub repository automatically: %s", str(err)) return None -def handle_commit_from_user(project, user_commit_summary: UserCommitSummary, signed: List[UserCommitSummary], - missing: List[UserCommitSummary]): # pylint: disable=too-many-arguments +def handle_commit_from_user( + project, user_commit_summary: UserCommitSummary, signed: List[UserCommitSummary], missing: List[UserCommitSummary] +): # pylint: disable=too-many-arguments """ Helper method to triage commits between signed and not-signed user signatures. @@ -1069,7 +1167,7 @@ def handle_commit_from_user(project, user_commit_summary: UserCommitSummary, sig :type: List[UserCommitSummary] """ - fn = 'cla.models.github_models.handle_commit_from_user' + fn = "cla.models.github_models.handle_commit_from_user" # handle edge case of non existant users if not user_commit_summary.is_valid_user(): missing.append(user_commit_summary) @@ -1080,26 +1178,31 @@ def handle_commit_from_user(project, user_commit_summary: UserCommitSummary, sig users = cla.utils.get_user_instance().get_user_by_github_id(user_commit_summary.author_id) if users is None: # GitHub user not in system yet, signature does not exist for this user. - cla.log.debug(f'{fn} - User commit summary: {user_commit_summary} ' - f'lookup by github numeric id not found in our database, ' - 'attempting to looking up user by email...') + cla.log.debug( + f"{fn} - User commit summary: {user_commit_summary} " + f"lookup by github numeric id not found in our database, " + "attempting to looking up user by email..." + ) # Try looking up user by email as a fallback users = cla.utils.get_user_instance().get_user_by_email(user_commit_summary.author_email) if users is None: # Try looking up user by github username - cla.log.debug(f'{fn} - User commit summary: {user_commit_summary} ' - f'lookup by github email not found in our database, ' - 'attempting to looking up user by github username...') + cla.log.debug( + f"{fn} - User commit summary: {user_commit_summary} " + f"lookup by github email not found in our database, " + "attempting to looking up user by github username..." + ) users = cla.utils.get_user_instance().get_user_by_github_username(user_commit_summary.author_login) # Got one or more records by searching the email or username if users is not None: - cla.log.debug(f'{fn} - Found {len(users)} GitHub user(s) matching ' - f'github email: {user_commit_summary.author_email}') + cla.log.debug( + f"{fn} - Found {len(users)} GitHub user(s) matching " f"github email: {user_commit_summary.author_email}" + ) for user in users: - cla.log.debug(f'{fn} - GitHub user found in our database: {user}') + cla.log.debug(f"{fn} - GitHub user found in our database: {user}") # For now, accept non-github users as legitimate users. # Does this user have a signed signature for this project? If so, add to the signed list and return, @@ -1114,8 +1217,9 @@ def handle_commit_from_user(project, user_commit_summary: UserCommitSummary, sig missing.append(user_commit_summary) else: # Not seen this user before - no record on file in our user's database - cla.log.debug(f'{fn} - User commit summary: {user_commit_summary} ' - f'lookup by email in our database failed - not found') + cla.log.debug( + f"{fn} - User commit summary: {user_commit_summary} " f"lookup by email in our database failed - not found" + ) # This bit of logic below needs to be reconsidered - query logic takes a very long time for large # projects like CNCF which significantly delays updating the GH PR status. @@ -1150,15 +1254,18 @@ def handle_commit_from_user(project, user_commit_summary: UserCommitSummary, sig # author_info consists of: [author_id, author_login, author_username, author_email] missing.append(user_commit_summary) else: - cla.log.debug(f'{fn} - Found {len(users)} GitHub user(s) matching ' - f'github id: {user_commit_summary.author_id} in our database') + cla.log.debug( + f"{fn} - Found {len(users)} GitHub user(s) matching " + f"github id: {user_commit_summary.author_id} in our database" + ) if len(users) > 1: - cla.log.warning(f'{fn} - more than 1 user found in our user database - user: {users} - ' - f'will ONLY evaluate the first one') + cla.log.warning( + f"{fn} - more than 1 user found in our user database - user: {users} - " f"will ONLY evaluate the first one" + ) # Just review the first user that we were able to fetch from our DB user = users[0] - cla.log.debug(f'{fn} - GitHub user found in our database: {user}') + cla.log.debug(f"{fn} - GitHub user found in our database: {user}") # Does this user have a signed signature for this project? If so, add to the signed list and return, # no reason to continue looking @@ -1182,35 +1289,39 @@ def handle_commit_from_user(project, user_commit_summary: UserCommitSummary, sig project_id=project.get_project_id(), signature_signed=True, signature_approved=True, - signature_type='ccla', - signature_reference_type='company', + signature_type="ccla", + signature_reference_type="company", signature_reference_id=user.get_user_company_id(), signature_user_ccla_company_id=None, ) # Should only return one signature record - cla.log.debug(f'{fn} - Found {len(signatures)} CCLA signatures for company: {user.get_user_company_id()}, ' - f'project: {project.get_project_id()} in our database.') + cla.log.debug( + f"{fn} - Found {len(signatures)} CCLA signatures for company: {user.get_user_company_id()}, " + f"project: {project.get_project_id()} in our database." + ) # Should never happen - warn if we see this if len(signatures) > 1: - cla.log.warning( - f'{fn} - more than 1 CCLA signature record found in our database - signatures: {signatures}') + cla.log.warning(f"{fn} - more than 1 CCLA signature record found in our database - signatures: {signatures}") for signature in signatures: if cla.utils.is_approved( - signature, - email=user_commit_summary.author_email, - github_id=user_commit_summary.author_id, - github_username=user_commit_summary.author_login # double check this... + signature, + email=user_commit_summary.author_email, + github_id=user_commit_summary.author_id, + github_username=user_commit_summary.author_login, # double check this... ): - cla.log.debug(f'{fn} - User Commit Summary: {user_commit_summary}, ' - 'is on one of the approval lists, but not affiliated with a company') + cla.log.debug( + f"{fn} - User Commit Summary: {user_commit_summary}, " + "is on one of the approval lists, but not affiliated with a company" + ) user_commit_summary.authorized = True break missing.append(user_commit_summary) + def get_merge_group_commit_authors(merge_group_sha, installation_id=None) -> List[UserCommitSummary]: """ Helper function to extract all committer information for a GitHub merge group. @@ -1220,42 +1331,39 @@ def get_merge_group_commit_authors(merge_group_sha, installation_id=None) -> Lis :return: A list of User Commit Summary objects containing the commit sha and available user information """ - fn = 'cla.models.github_models.get_merge_group_commit_authors' - cla.log.debug(f'Querying merge group {merge_group_sha} for commit authors...') + fn = "cla.models.github_models.get_merge_group_commit_authors" + cla.log.debug(f"Querying merge group {merge_group_sha} for commit authors...") commit_authors = [] try: g = cla.utils.get_github_integration_instance(installation_id=installation_id) commit = g.get_commit(merge_group_sha) for parent in commit.parents: try: - cla.log.debug(f'{fn} - Querying parent commit {parent.sha} for commit authors...') + cla.log.debug(f"{fn} - Querying parent commit {parent.sha} for commit authors...") commit = g.get_commit(parent.sha) - cla.log.debug(f'{fn} - Found {commit.commit.author.name} as the author of parent commit {parent.sha}') - commit_authors.append(UserCommitSummary( - parent.sha, - commit.author.id, - commit.author.login, - commit.author.name, - commit.author.email, - False, False - )) + cla.log.debug(f"{fn} - Found {commit.commit.author.name} as the author of parent commit {parent.sha}") + commit_authors.append( + UserCommitSummary( + parent.sha, + commit.author.id, + commit.author.login, + commit.author.name, + commit.author.email, + False, + False, + ) + ) except (GithubException, IncompletableObject) as e: - cla.log.warning(f'{fn} - Unable to query parent commit {parent.sha} for commit authors: {e}') - commit_authors.append(UserCommitSummary( - parent.sha, - None, - None, - None, - None, - False, False - )) - + cla.log.warning(f"{fn} - Unable to query parent commit {parent.sha} for commit authors: {e}") + commit_authors.append(UserCommitSummary(parent.sha, None, None, None, None, False, False)) + except Exception as e: - cla.log.warning(f'{fn} - Unable to query merge group {merge_group_sha} for commit authors: {e}') + cla.log.warning(f"{fn} - Unable to query merge group {merge_group_sha} for commit authors: {e}") return commit_authors - -def get_author_summary(commit,pr,installation_id) -> List[UserCommitSummary]: + + +def get_author_summary(commit, pr, installation_id) -> List[UserCommitSummary]: """ Helper function to extract author information from a GitHub commit. :param commit: A GitHub commit object. @@ -1263,30 +1371,33 @@ def get_author_summary(commit,pr,installation_id) -> List[UserCommitSummary]: :param pr: PR number :type pr: int """ - fn = 'cla.models.github_models.get_author_summary' + fn = "cla.models.github_models.get_author_summary" commit_authors = [] if commit.author: try: commit_author_summary = UserCommitSummary( - commit.sha, - commit.author.id, - commit.author.login, - commit.author.name, - commit.author.email, - False, False # default not authorized - will be evaluated and updated later + commit.sha, + commit.author.id, + commit.author.login, + commit.author.name, + commit.author.email, + False, + False, # default not authorized - will be evaluated and updated later ) - cla.log.debug(f'{fn} - PR: {pr}, {commit_author_summary}') + cla.log.debug(f"{fn} - PR: {pr}, {commit_author_summary}") # check for co-author details # issue # 3884 commit_authors.append(commit_author_summary) co_authors = cla.utils.get_co_authors_from_commit(commit) with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: for co_author in co_authors: - commit_authors.append(executor.submit(get_co_author_commits, co_author, commit, pr, installation_id).result()) + commit_authors.append( + executor.submit(get_co_author_commits, co_author, commit, pr, installation_id).result() + ) return commit_authors except (GithubException, IncompletableObject) as exc: - cla.log.warning(f'{fn} - PR: {pr}, unable to get commit author summary: {exc}') + cla.log.warning(f"{fn} - PR: {pr}, unable to get commit author summary: {exc}") try: # commit.commit.author is a github.GitAuthor.GitAuthor object type - object # only has date, name and email attributes - no ID attribute/value @@ -1297,44 +1408,30 @@ def get_author_summary(commit,pr,installation_id) -> List[UserCommitSummary]: None, commit.commit.author.name, commit.commit.author.email, - False, False # default not authorized - will be evaluated and updated later + False, + False, # default not authorized - will be evaluated and updated later + ) + cla.log.debug(f"{fn} - github.GitAuthor.GitAuthor object: {commit.commit.author}") + cla.log.debug( + f"{fn} - PR: {pr}, " + f"GitHub NamedUser author NOT found for commit SHA {commit_author_summary} " + f"however, we did find GitAuthor info" ) - cla.log.debug(f'{fn} - github.GitAuthor.GitAuthor object: {commit.commit.author}') - cla.log.debug(f'{fn} - PR: {pr}, ' - f'GitHub NamedUser author NOT found for commit SHA {commit_author_summary} ' - f'however, we did find GitAuthor info') - cla.log.debug(f'{fn} - PR: {pr}, {commit_author_summary}') + cla.log.debug(f"{fn} - PR: {pr}, {commit_author_summary}") commit_authors.append(commit_author_summary) return commit_authors except (GithubException, IncompletableObject) as exc: - cla.log.warning(f'{fn} - PR: {pr}, unable to get commit author summary: {exc}') - commit_author_summary = UserCommitSummary( - commit.sha, - None, - None, - None, - None, - False, False - ) - cla.log.warning(f'{fn} - PR: {pr}, ' - f'could not find any commit author for SHA {commit_author_summary}') + cla.log.warning(f"{fn} - PR: {pr}, unable to get commit author summary: {exc}") + commit_author_summary = UserCommitSummary(commit.sha, None, None, None, None, False, False) + cla.log.warning(f"{fn} - PR: {pr}, " f"could not find any commit author for SHA {commit_author_summary}") commit_authors.append(commit_author_summary) return commit_authors else: - cla.log.warning(f'{fn} - PR: {pr}, ' - f'could not find any commit author for SHA {commit.sha}') - commit_author_summary = UserCommitSummary( - commit.sha, - None, - None, - None, - None, - False, False - ) + cla.log.warning(f"{fn} - PR: {pr}, " f"could not find any commit author for SHA {commit.sha}") + commit_author_summary = UserCommitSummary(commit.sha, None, None, None, None, False, False) commit_authors.append(commit_author_summary) return commit_authors - - + def get_pull_request_commit_authors(pull_request, installation_id) -> List[UserCommitSummary]: """ @@ -1352,44 +1449,48 @@ def get_pull_request_commit_authors(pull_request, installation_id) -> List[UserC :return: A list of User Commit Summary objects containing the commit sha and available user information :rtype: List[UserCommitSummary] """ - fn = 'cla.models.github_models.get_pull_request_commit_authors' - cla.log.debug(f'{fn} - Querying pull request commits for author information...') + fn = "cla.models.github_models.get_pull_request_commit_authors" + cla.log.debug(f"{fn} - Querying pull request commits for author information...") no_commits = pull_request.get_commits().totalCount - cla.log.debug(f'{fn} - PR: {pull_request.number}, number of commits: {no_commits}') - + cla.log.debug(f"{fn} - PR: {pull_request.number}, number of commits: {no_commits}") + commit_authors = [] with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: - future_to_commit = {executor.submit(get_author_summary, commit, pull_request.number, installation_id): commit for commit in pull_request.get_commits()} + future_to_commit = { + executor.submit(get_author_summary, commit, pull_request.number, installation_id): commit + for commit in pull_request.get_commits() + } for future in concurrent.futures.as_completed(future_to_commit): future_to_commit[future] try: commit_authors.extend(future.result()) except Exception as exc: - cla.log.warning(f'{fn} - PR: {pull_request.number}, get_author_summary generated an exception: {exc}') + cla.log.warning(f"{fn} - PR: {pull_request.number}, get_author_summary generated an exception: {exc}") raise exc - + return commit_authors -def get_co_author_commits(co_author,commit,pr,installation_id): - fn = 'cla.models.github_models.get_co_author_commits' +def get_co_author_commits(co_author, commit, pr, installation_id): + fn = "cla.models.github_models.get_co_author_commits" # check if co-author is a github user + co_author_summary = None login, github_id = None, None email = co_author[1] name = co_author[0] # get repository service - github = cla.utils.get_repository_service('github') - cla.log.debug(f'{fn} - getting co-author details: {co_author}, email: {email}, name: {name}') + github = cla.utils.get_repository_service("github") + cla.log.debug(f"{fn} - getting co-author details: {co_author}, email: {email}, name: {name}") try: user = github.get_github_user_by_email(email, installation_id) except (GithubException, IncompletableObject, RateLimitExceededException) as ex: # user not found - cla.log.debug(f'{fn} - co-author github user not found : {co_author} with exception: {ex}') + cla.log.debug(f"{fn} - co-author github user not found : {co_author} with exception: {ex}") user = None - cla.log.debug(f'{fn} - co-author: {co_author}, user: {user}') + cla.log.debug(f"{fn} - co-author: {co_author}, user: {user}") if user: - cla.log.debug(f'{fn} - co-author github user details found : {co_author}, user: {user}') + cla.log.debug(f"{fn} - co-author github user details found : {co_author}, user: {user}") login = user.login github_id = user.id co_author_summary = UserCommitSummary( @@ -1398,12 +1499,18 @@ def get_co_author_commits(co_author,commit,pr,installation_id): login, name, email, - False, False # default not authorized - will be evaluated and updated later + False, + False, # default not authorized - will be evaluated and updated later ) - cla.log.debug(f'{fn} - PR: {pr}, {co_author_summary}') - return co_author_summary + cla.log.debug(f"{fn} - PR: {pr}, {co_author_summary}") else: - cla.log.debug(f'{fn} - co-author github user details not found : {co_author}') + co_author_summary = UserCommitSummary( + commit.sha, None, None, name, email, False, False # default not authorized - will be evaluated and updated later + ) + cla.log.debug(f"{fn} - co-author github user details not found : {co_author}") + + return co_author_summary + def has_check_previously_passed_or_failed(pull_request: PullRequest): """ @@ -1419,22 +1526,28 @@ def has_check_previously_passed_or_failed(pull_request: PullRequest): for comment in comments: # Our bot comments include the following text # A previously failed check has 'not authorized' somewhere in the body - if 'is not authorized under a signed CLA' in comment.body: + if "is not authorized under a signed CLA" in comment.body: return True, comment - if 'they must confirm their affiliation' in comment.body: + if "they must confirm their affiliation" in comment.body: return True, comment - if 'is missing the User' in comment.body: + if "is missing the User" in comment.body: return True, comment - if 'are authorized under a signed CLA' in comment.body: + if "are authorized under a signed CLA" in comment.body: return True, comment - if 'is not linked to the GitHub account' in comment.body: + if "is not linked to the GitHub account" in comment.body: return True, comment return False, None -def update_pull_request(installation_id, github_repository_id, pull_request, repository_name, - signed: List[UserCommitSummary], - missing: List[UserCommitSummary], project_version): # pylint: disable=too-many-locals +def update_pull_request( + installation_id, + github_repository_id, + pull_request, + repository_name, + signed: List[UserCommitSummary], + missing: List[UserCommitSummary], + project_version, +): # pylint: disable=too-many-locals """ Helper function to update a PR's comment and status based on the list of signers. @@ -1453,28 +1566,28 @@ def update_pull_request(installation_id, github_repository_id, pull_request, rep :param: project_version: Project version associated with PR :type: missing: string """ - fn = 'cla.models.github_models.update_pull_request' - notification = cla.conf['GITHUB_PR_NOTIFICATION'] - both = notification == 'status+comment' or notification == 'comment+status' + fn = "cla.models.github_models.update_pull_request" + notification = cla.conf["GITHUB_PR_NOTIFICATION"] + both = notification == "status+comment" or notification == "comment+status" last_commit = pull_request.get_commits().reversed[0] # Here we update the PR status by adding/updating the PR body - this is the way the EasyCLA app # knows if it is pass/fail. # Create check run for users that haven't yet signed and/or affiliated if missing: - text = '' - help_url = '' - + text = "" + help_url = "" + for user_commit_summary in missing: # Check for valid GitHub id # old tuple: (sha, (author_id, author_login_or_name, author_email, optionalTrue)) if not user_commit_summary.is_valid_user(): help_url = "https://help.github.com/en/github/committing-changes-to-your-project/why-are-my-commits-linked-to-the-wrong-user" else: - help_url = cla.utils.get_full_sign_url('github', str(installation_id), github_repository_id, - pull_request.number, project_version) + help_url = cla.utils.get_full_sign_url( + "github", str(installation_id), github_repository_id, pull_request.number, project_version + ) - # check if unsigned user is whitelisted if user_commit_summary.commit_sha != last_commit.sha: continue @@ -1497,75 +1610,88 @@ def update_pull_request(installation_id, github_repository_id, pull_request, rep client.create_check_run(repository_name, json.dumps(payload)) # Update the comment - if both or notification == 'comment': - body = cla.utils.assemble_cla_comment('github', str(installation_id), github_repository_id, pull_request.number, - signed, missing, project_version) + if both or notification == "comment": + body = cla.utils.assemble_cla_comment( + "github", str(installation_id), github_repository_id, pull_request.number, signed, missing, project_version + ) previously_pass_or_failed, comment = has_check_previously_passed_or_failed(pull_request) if not missing: # After Issue #167 wsa in place, they decided via Issue #289 that we # DO want to update the comment, but only after we've previously failed if previously_pass_or_failed: - cla.log.debug(f'{fn} - Found previously passed or failed checks - updating CLA comment in PR.') + cla.log.debug(f"{fn} - Found previously passed or failed checks - updating CLA comment in PR.") comment.edit(body) - cla.log.debug(f'{fn} - EasyCLA App checks pass for PR: {pull_request.number} with authors: {signed}') + cla.log.debug(f"{fn} - EasyCLA App checks pass for PR: {pull_request.number} with authors: {signed}") else: # Per Issue #167, only add a comment if check fails # update_cla_comment(pull_request, body) if previously_pass_or_failed: - cla.log.debug(f'{fn} - Found previously failed checks - updating CLA comment in PR.') + cla.log.debug(f"{fn} - Found previously failed checks - updating CLA comment in PR.") comment.edit(body) else: pull_request.create_issue_comment(body) - cla.log.debug(f'{fn} - EasyCLA App checks fail for PR: {pull_request.number}. ' - f'CLA signatures with signed authors: {signed} and ' - f'with missing authors: {missing}') + cla.log.debug( + f"{fn} - EasyCLA App checks fail for PR: {pull_request.number}. " + f"CLA signatures with signed authors: {signed} and " + f"with missing authors: {missing}" + ) - if both or notification == 'status': - context_name = os.environ.get('GH_STATUS_CTX_NAME') + if both or notification == "status": + context_name = os.environ.get("GH_STATUS_CTX_NAME") if context_name is None: - context_name = 'communitybridge/cla' + context_name = "communitybridge/cla" # if we have ANY committers who have failed the check - update the status with overall failure if missing is not None and len(missing) > 0: - state = 'failure' + state = "failure" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=False) sign_url = cla.utils.get_full_sign_url( - 'github', str(installation_id), github_repository_id, pull_request.number, project_version) - cla.log.debug(f'{fn} - Creating new CLA \'{state}\' status - {len(signed)} passed, {missing} failed, ' - f'signing url: {sign_url}') + "github", str(installation_id), github_repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) create_commit_status(pull_request, last_commit.sha, state, sign_url, body, context) elif signed is not None and len(signed) > 0: - state = 'success' + state = "success" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=True) sign_url = cla.conf["CLA_LANDING_PAGE"] # Remove this once signature detail page ready. sign_url = os.path.join(sign_url, "#/") sign_url = append_project_version_to_url(address=sign_url, project_version=project_version) - cla.log.debug(f'{fn} - Creating new CLA \'{state}\' status - {len(signed)} passed, {missing} failed, ' - f'signing url: {sign_url}') + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) create_commit_status(pull_request, last_commit.sha, state, sign_url, body, context) else: # error condition - should have at least one committer, and they would be in one of the above # lists: missing or signed - state = 'failure' + state = "failure" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=False) sign_url = cla.utils.get_full_sign_url( - 'github', str(installation_id), github_repository_id, pull_request.number, project_version) - cla.log.debug(f'{fn} - Creating new CLA \'{state}\' status - {len(signed)} passed, {missing} failed, ' - f'signing url: {sign_url}') - cla.log.warning('{fn} - This is an error condition - ' - f'should have at least one committer in one of these lists: ' - f'{len(signed)} passed, {missing}') + "github", str(installation_id), github_repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) + cla.log.warning( + "{fn} - This is an error condition - " + f"should have at least one committer in one of these lists: " + f"{len(signed)} passed, {missing}" + ) create_commit_status(pull_request, last_commit.sha, state, sign_url, body, context) -def create_commit_status_for_merge_group(commit_obj,merge_commit_sha, state, sign_url, body, context): +def create_commit_status_for_merge_group(commit_obj, merge_commit_sha, state, sign_url, body, context): """ Helper function to create a pull request commit status message. @@ -1582,12 +1708,12 @@ def create_commit_status_for_merge_group(commit_obj,merge_commit_sha, state, sig """ try: # Create status - cla.log.debug(f'Creating commit status for merge commit {merge_commit_sha}') + cla.log.debug(f"Creating commit status for merge commit {merge_commit_sha}") commit_obj.create_status(state=state, target_url=sign_url, description=body, context=context) except Exception as e: - cla.log.warning(f'Unable to create commit status for ' - f'and merge commit {merge_commit_sha}: {e}') + cla.log.warning(f"Unable to create commit status for " f"and merge commit {merge_commit_sha}: {e}") + def create_commit_status(pull_request, commit_hash, state, sign_url, body, context): """ @@ -1611,22 +1737,26 @@ def create_commit_status(pull_request, commit_hash, state, sign_url, body, conte commit_obj = commit break if commit_obj is None: - cla.log.error(f'Could not post status {state} on ' - f'PR: {pull_request.number}, ' - f'Commit: {commit_hash} not found') + cla.log.error( + f"Could not post status {state} on " f"PR: {pull_request.number}, " f"Commit: {commit_hash} not found" + ) return # context is a string label to differentiate one signer status from another signer status. # committer name is used as context label - cla.log.info(f'Updating status with state \'{state}\' on PR {pull_request.number} for commit {commit_hash}...') + cla.log.info(f"Updating status with state '{state}' on PR {pull_request.number} for commit {commit_hash}...") # returns github.CommitStatus.CommitStatus resp = commit_obj.create_status(state, sign_url, body, context) - cla.log.info(f'Successfully posted status \'{state}\' on PR {pull_request.number}: Commit {commit_hash} ' - f'with SignUrl : {sign_url} with response: {resp}') + cla.log.info( + f"Successfully posted status '{state}' on PR {pull_request.number}: Commit {commit_hash} " + f"with SignUrl : {sign_url} with response: {resp}" + ) except GithubException as exc: - cla.log.error(f'Could not post status \'{state}\' on PR: {pull_request.number}, ' - f'Commit: {commit_hash}, ' - f'Response Code: {exc.status}, ' - f'Message: {exc.data}') + cla.log.error( + f"Could not post status '{state}' on PR: {pull_request.number}, " + f"Commit: {commit_hash}, " + f"Response Code: {exc.status}, " + f"Message: {exc.data}" + ) # def update_cla_comment(pull_request, body): @@ -1688,26 +1818,28 @@ def _get_github_client(self, username, token): return MockGitHubClient(username, token) def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url): - authorization_url = 'http://authorization.url' - state = 'random-state-here' + authorization_url = "http://authorization.url" + state = "random-state-here" return authorization_url, state def _fetch_token(self, client_id, state, token_url, client_secret, code): # pylint: disable=too-many-arguments - return 'random-token' + return "random-token" def _get_request_session(self, request) -> dict: if self.oauth2_token: - return {'github_oauth2_token': 'random-token', - 'github_oauth2_state': 'random-state', - 'github_origin_url': 'http://github/origin/url', - 'github_installation_id': 1} + return { + "github_oauth2_token": "random-token", + "github_oauth2_state": "random-state", + "github_origin_url": "http://github/origin/url", + "github_installation_id": 1, + } return {} def get_user_data(self, session, client_id) -> dict: - return {'email': 'test@user.com', 'name': 'Test User', 'id': 123} + return {"email": "test@user.com", "name": "Test User", "id": 123} def get_user_emails(self, session, client_id): - return [{'email': 'test@user.com', 'verified': True, 'primary': True, 'visibility': 'public'}] + return [{"email": "test@user.com", "verified": True, "primary": True, "visibility": "public"}] def get_pull_request(self, github_repository_id, pull_request_number, installation_id): return MockGitHubPullRequest(pull_request_number) @@ -1751,7 +1883,7 @@ class MockGitHubPullRequest(object): # pylint: disable=too-few-public-methods def __init__(self, pull_request_id): self.number = pull_request_id - self.html_url = 'http://test-github.com/user/repo/' + str(self.number) + self.html_url = "http://test-github.com/user/repo/" + str(self.number) def get_commits(self): # pylint: disable=no-self-use """ @@ -1778,7 +1910,8 @@ class MockGitHubComment(object): # pylint: disable=too-few-public-methods """ A GitHub mock issue comment object for testing. """ - body = 'Test' + + body = "Test" class MockPaginatedList(github.PaginatedList.PaginatedListBase): # pylint: disable=too-few-public-methods @@ -1807,7 +1940,7 @@ class MockGitHubCommit(object): # pylint: disable=too-few-public-methods def __init__(self): self.author = MockGitHubAuthor() - self.sha = 'sha-test-commit' + self.sha = "sha-test-commit" def create_status(self, state, sign_url, body): """ @@ -1823,5 +1956,5 @@ class MockGitHubAuthor(object): # pylint: disable=too-few-public-methods def __init__(self, author_id=1): self.id = author_id - self.login = 'user' - self.email = 'user@github.com' + self.login = "user" + self.email = "user@github.com" diff --git a/cla-backend/cla/tests/unit/test_github_models.py b/cla-backend/cla/tests/unit/test_github_models.py index 833d4d329..533744b62 100644 --- a/cla-backend/cla/tests/unit/test_github_models.py +++ b/cla-backend/cla/tests/unit/test_github_models.py @@ -1,257 +1,13 @@ # Copyright The Linux Foundation and each contributor to CommunityBridge. # SPDX-License-Identifier: MIT -# TODO - Need to mock this set of tests so that it doesn't require the real service -# import logging -# import unittest -# from unittest.mock import patch, MagicMock -# -# from github import Github -# -# import cla -# from cla.models.dynamo_models import Signature, Project -# from cla.models.github_models import GitHub as GithubModel -# from cla.models.github_models import get_pull_request_commit_authors, handle_commit_from_user, MockGitHub -# from cla.user import UserCommitSummary -# -# -# class TestGitHubModels(unittest.TestCase): -# -# @classmethod -# def setUpClass(cls) -> None: -# cls.mock_user_patcher = patch('cla.models.github_models.cla.utils.get_user_instance') -# cls.mock_signature_patcher = patch('cla.models.github_models.cla.utils.get_signature_instance') -# cls.mock_utils_patcher = patch('cla.models.github_models.cla.utils') -# cls.mock_utils_get = cls.mock_utils_patcher.start() -# cls.mock_user_get = cls.mock_user_patcher.start() -# cls.mock_signature_get = cls.mock_signature_patcher.start() -# -# @classmethod -# def tearDownClass(cls) -> None: -# cls.mock_user_patcher.stop() -# cls.mock_signature_patcher.stop() -# cls.mock_utils_patcher.stop() -# -# def setUp(self) -> None: -# # Only show critical logging stuff -# cla.log.level = logging.CRITICAL -# #self.assertTrue(cla.conf['GITHUB_OAUTH_TOKEN'] != '', -# # 'Missing GITHUB_OAUTH_TOKEN environment variable - required to run unit tests') -# # cla.log.debug('Using GITHUB_OAUTH_TOKEN: {}...'.format(cla.conf['GITHUB_OAUTH_TOKEN'][:5])) -# -# def tearDown(self) -> None: -# pass -# -# @unittest.skip("todo - need to mock GitHub service") -# def test_commit_authors_with_named_user(self) -> None: -# """ -# Test that we can load commit authors from a pull request that does have the traditional -# github.NamedUser.NamedUser object filled out -# """ -# g = Github(cla.conf['GITHUB_OAUTH_TOKEN']) -# repo = g.get_repo(27729926) # grpc/grpc-java -# pr = repo.get_pull(6142) # example: https://github.com/grpc/grpc-java/pull/6142 -# cla.log.info("Retrieved GitHub PR: {}".format(pr)) -# commits = pr.get_comments() -# cla.log.info("Retrieved GitHub PR: {}, commits: {}".format(pr, commits)) -# -# # Returns a list tuples, which look like (commit_sha_string, (author_id, author_username, author_email), -# # which, as you can see, the second element of the tuple is another tuple containing the author information -# commit_authors = get_pull_request_commit_authors(pr) -# # cla.log.info("Result: {}".format(commit_authors)) -# # cla.log.info([author_info[1] for commit, author_info in commit_authors]) -# self.assertTrue(4779759 in [user_commit_summary.author_id for user_commit_summary in commit_authors]) -# -# @unittest.skip("todo - need to mock GitHub service") -# def test_commit_authors_no_named_user(self) -> None: -# """ -# Test that we can load commit authors from a pull request that does NOT have the traditional -# github.NamedUser.NamedUser object filled out -# """ -# # We need to mock this service so that we can test our business logic - disabling this test for now -# # as they closed the PR -# g = Github(cla.conf['GITHUB_OAUTH_TOKEN']) -# repo = g.get_repo(27729926) # grpc/grpc-java -# pr = repo.get_pull(6152) # example: https://github.com/grpc/grpc-java/pull/6152 -# cla.log.info("Retrieved GitHub PR: {}".format(pr)) -# commits = pr.get_comments() -# cla.log.info("Retrieved GitHub PR: {}, commits: {}".format(pr, commits)) -# -# # Returns a list tuples, which look like (commit_sha_string, (author_id, author_username, author_email), -# # which, as you can see, the second element of the tuple is another tuple containing the author information -# # commit_authors = get_pull_request_commit_authors(pr) -# # cla.log.info("Result: {}".format(commit_authors)) -# # cla.log.info([author_info[1] for commit, author_info in commit_authors]) -# # self.assertTrue('snalkar' in [author_info[1] for commit, author_info in commit_authors]) -# -# def test_handle_commit_author_whitelisted(self) -> None: -# """ -# Test case where commit authors have no signatures but have been whitelisted and should -# return missing list containing a whitelisted flag -# """ -# # Mock user not existing and happens to be whitelisted -# self.mock_user_get.return_value.get_user_by_github_id.return_value = None -# self.mock_user_get.return_value.get_user_by_email.return_value = None -# self.mock_signature_get.return_value.get_signatures_by_project.return_value = [Signature()] -# self.mock_utils_get.return_value.is_approved.return_value = True -# user_commit_summary = UserCommitSummary('fake_sha', 123, 'foo', None, 'foo@gmail.com', True, True) -# missing = [] -# signed = [] -# project = Project() -# project.set_project_id('fake_project_id') -# handle_commit_from_user(project, user_commit_summary, signed, missing) -# # We commented out this functionality for now - re-enable if we add it back -# self.assertEqual(missing, [user_commit_summary]) -# self.assertEqual(signed, []) -# -# def test_handle_invalid_author(self) -> None: -# """ -# Test case handling non-existent author tagged to a given commit -# """ -# project = Project() -# author_info = UserCommitSummary('fake_sha', None, None, None, None, False, False) -# signed = [] -# missing = [] -# handle_commit_from_user(project, author_info, signed, missing) -# self.assertEqual(signed, []) -# self.assertEqual(missing, [author_info]) -# -# -# class TestGithubModelsPrComment(unittest.TestCase): -# -# def setUp(self) -> None: -# self.github = MockGitHub() -# self.github.update_change_request = MagicMock() -# -# def tearDown(self) -> None: -# pass -# -# def test_process_easycla_command_comment(self): -# with self.assertRaisesRegex(ValueError, "missing comment body"): -# self.github.process_easycla_command_comment({}) -# -# with self.assertRaisesRegex(ValueError, "unsupported comment supplied"): -# self.github.process_easycla_command_comment({ -# "comment": {"body": "/otherbot"} -# }) -# -# with self.assertRaisesRegex(ValueError, "missing github repository id"): -# self.github.process_easycla_command_comment({ -# "comment": {"body": "/easycla"}, -# }) -# -# with self.assertRaisesRegex(ValueError, "missing pull request id"): -# self.github.process_easycla_command_comment({ -# "comment": {"body": "/easycla"}, -# "repository": {"id": 123}, -# }) -# -# with self.assertRaisesRegex(ValueError, "missing installation id"): -# self.github.process_easycla_command_comment({ -# "comment": {"body": "/easycla"}, -# "repository": {"id": 123}, -# "issue": {"number": 1}, -# }) -# -# self.github.process_easycla_command_comment({ -# "comment": {"body": "/easycla"}, -# "repository": {"id": 123}, -# "issue": {"number": 1}, -# "installation": {"id": 1}, -# }) -# -# -# class TestGithubUserEmails(unittest.TestCase): -# -# def test_empty_emails(self): -# with patch.object(GithubModel, "_fetch_github_emails") as _fetch_github_emails: -# _fetch_github_emails.return_value = [] -# github = GithubModel() -# emails = github.get_user_emails(None, "fake_client_id") -# assert not emails -# -# def test_emails_with_noreply(self): -# with patch.object(GithubModel, "_fetch_github_emails") as _fetch_github_emails: -# _fetch_github_emails.return_value = [ -# { -# "email": "octocat@users.noreply.github.com", -# "verified": True, -# "primary": True, -# "visibility": "public" -# }, -# { -# "email": "pumacat@gmail.com", -# "verified": True, -# "primary": True, -# "visibility": "public" -# }, -# { -# "email": "pumacat+notveried@gmail.com", -# "verified": False, -# "primary": True, -# "visibility": "public" -# } -# ] -# github = GithubModel() -# emails = github.get_user_emails(None, "fake_client_id") -# assert emails -# assert len(emails) == 1 -# assert emails == ["pumacat@gmail.com"] -# -# def test_emails_with_noreply_single(self): -# with patch.object(GithubModel, "_fetch_github_emails") as _fetch_github_emails: -# _fetch_github_emails.return_value = [ -# { -# "email": "octocat@users.noreply.github.com", -# "verified": True, -# "primary": True, -# "visibility": "public" -# }, -# ] -# github = GithubModel() -# emails = github.get_user_emails(None, "fake_client_id") -# assert emails -# assert len(emails) == 1 -# assert emails == ["octocat@users.noreply.github.com"] -# -# def test_emails_without_noreply(self): -# with patch.object(GithubModel, "_fetch_github_emails") as _fetch_github_emails: -# _fetch_github_emails.return_value = [ -# { -# "email": "pumacat@gmail.com", -# "verified": True, -# "primary": True, -# "visibility": "public" -# }, -# { -# "email": "pumacat2@gmail.com", -# "verified": True, -# "primary": True, -# "visibility": "public" -# }, -# { -# "email": "pumacat+notveried@gmail.com", -# "verified": False, -# "primary": True, -# "visibility": "public" -# } -# ] -# github = GithubModel() -# emails = github.get_user_emails(None, "fake_client_id") -# assert emails -# assert len(emails) == 2 -# assert "pumacat@gmail.com" in emails -# assert "pumacat2@gmail.com" in emails -# -# -# if __name__ == '__main__': -# unittest.main() import unittest from unittest import TestCase from unittest.mock import MagicMock, Mock, patch -from cla.models.github_models import UserCommitSummary, get_author_summary, get_pull_request_commit_authors -from github import NamedUser +from cla.models.github_models import (UserCommitSummary, get_author_summary, + get_co_author_commits, + get_pull_request_commit_authors) class TestGetPullRequestCommitAuthors(TestCase): @@ -275,13 +31,8 @@ def test_get_pull_request_commit_with_co_author(self, mock_github_instance): commit.author.email = "fake_author@example.com" pull_request.get_commits.return_value.__iter__.return_value = [commit] - mock_user = Mock(spec=NamedUser) - mock_user.id = 2 - mock_user.login = "co_author_login" - - mock_user_2 = Mock(spec=NamedUser) - mock_user_2.id = 3 - mock_user_2.login = "co_author_login_2" + mock_user = Mock(id=2, login="co_author_login") + mock_user_2 = Mock(id=3, login="co_author_login_2") mock_github_instance.return_value.get_github_user_by_email.side_effect = ( lambda email, _: mock_user if email == co_author_email else mock_user_2 @@ -296,6 +47,48 @@ def test_get_pull_request_commit_with_co_author(self, mock_github_instance): self.assertIn(co_author_email_2, [author.author_email for author in result]) self.assertIn("fake_login", [author.author_login for author in result]) self.assertIn("co_author_login", [author.author_login for author in result]) + + @patch("cla.utils.get_repository_service") + def test_get_co_author_commits_invalid_gh_email(self, mock_github_instance): + # Mock data + co_author = ("co_author", "co_author_email.gmail.com") + commit = MagicMock() + commit.sha = "fake_sha" + mock_github_instance.return_value.get_github_user_by_email.return_value = None + pr = 1 + installation_id = 123 + + # Call the function + result = get_co_author_commits(co_author,commit, pr, installation_id) + + # Assertions + self.assertEqual(result.commit_sha, "fake_sha") + self.assertEqual(result.author_id, None) + self.assertEqual(result.author_login, None) + self.assertEqual(result.author_email, "co_author_email.gmail.com") + self.assertEqual(result.author_name, "co_author") + + @patch("cla.utils.get_repository_service") + def test_get_co_author_commits_valid_gh_email(self, mock_github_instance): + # Mock data + co_author = ("co_author", "co_author_email.gmail.com") + commit = MagicMock() + commit.sha = "fake_sha" + mock_github_instance.return_value.get_github_user_by_email.return_value = Mock( + id=123, login="co_author_login" + ) + pr = 1 + installation_id = 123 + + # Call the function + result = get_co_author_commits(co_author,commit, pr, installation_id) + + # Assertions + self.assertEqual(result.commit_sha, "fake_sha") + self.assertEqual(result.author_id, 123) + self.assertEqual(result.author_login, "co_author_login") + self.assertEqual(result.author_email, "co_author_email.gmail.com") + self.assertEqual(result.author_name, "co_author") if __name__ == "__main__": diff --git a/cla-backend/cla/utils.py b/cla-backend/cla/utils.py index de6b28dd5..c08fb9a1a 100644 --- a/cla-backend/cla/utils.py +++ b/cla-backend/cla/utils.py @@ -15,12 +15,9 @@ from typing import List, Optional from urllib.parse import urlencode +import cla import falcon import requests -from hug.middleware import SessionMiddleware -from requests_oauthlib import OAuth2Session - -import cla from cla.middleware import CLALogMiddleware from cla.models import DoesNotExist from cla.models.dynamo_models import (CCLAWhitelistRequest, CLAManagerRequest, @@ -30,11 +27,13 @@ User, UserPermissions) from cla.models.event_types import EventType from cla.user import UserCommitSummary +from hug.middleware import SessionMiddleware +from requests_oauthlib import OAuth2Session -API_BASE_URL = os.environ.get('CLA_API_BASE', '') -CLA_LOGO_URL = os.environ.get('CLA_BUCKET_LOGO_URL', '') -CORPORATE_BASE = os.environ.get('CLA_CORPORATE_BASE', '') -CORPORATE_V2_BASE = os.environ.get('CLA_CORPORATE_V2_BASE', '') +API_BASE_URL = os.environ.get("CLA_API_BASE", "") +CLA_LOGO_URL = os.environ.get("CLA_BUCKET_LOGO_URL", "") +CORPORATE_BASE = os.environ.get("CLA_CORPORATE_BASE", "") +CORPORATE_V2_BASE = os.environ.get("CLA_CORPORATE_V2_BASE", "") def get_cla_path(): @@ -45,16 +44,22 @@ def get_cla_path(): def get_log_middleware(): - """Prepare the hug middleware to manage logging. """ + """Prepare the hug middleware to manage logging.""" return CLALogMiddleware(logger=cla.log) def get_session_middleware(): """Prepares the hug middleware to manage key-value session data.""" store = get_key_value_store_service() - return SessionMiddleware(store, context_name='session', cookie_name='cla-sid', - cookie_max_age=300, cookie_domain=None, cookie_path='/', - cookie_secure=False) + return SessionMiddleware( + store, + context_name="session", + cookie_name="cla-sid", + cookie_max_age=300, + cookie_domain=None, + cookie_path="/", + cookie_secure=False, + ) def create_database(conf=None): @@ -67,11 +72,11 @@ def create_database(conf=None): """ if conf is None: conf = cla.conf - cla.log.info('Creating CLA database in %s', conf['DATABASE']) - if conf['DATABASE'] == 'DynamoDB': + cla.log.info("Creating CLA database in %s", conf["DATABASE"]) + if conf["DATABASE"] == "DynamoDB": from cla.models.dynamo_models import create_database as cd else: - raise Exception('Invalid database selection in configuration: %s' % conf['DATABASE']) + raise Exception("Invalid database selection in configuration: %s" % conf["DATABASE"]) cd() @@ -87,11 +92,11 @@ def delete_database(conf=None): """ if conf is None: conf = cla.conf - cla.log.warning('Deleting CLA database in %s', conf['DATABASE']) - if conf['DATABASE'] == 'DynamoDB': + cla.log.warning("Deleting CLA database in %s", conf["DATABASE"]) + if conf["DATABASE"] == "DynamoDB": from cla.models.dynamo_models import delete_database as dd else: - raise Exception('Invalid database selection in configuration: %s' % conf['DATABASE']) + raise Exception("Invalid database selection in configuration: %s" % conf["DATABASE"]) dd() @@ -111,15 +116,25 @@ def get_database_models(conf=None): """ if conf is None: conf = cla.conf - if conf['DATABASE'] == 'DynamoDB': - return {'User': User, 'Signature': Signature, 'Repository': Repository, - 'Company': Company, 'Project': Project, 'Document': Document, - 'GitHubOrg': GitHubOrg, 'Gerrit': Gerrit, 'UserPermissions': UserPermissions, - 'Event': Event, 'CompanyInvites': CompanyInvite, 'ProjectCLAGroup': ProjectCLAGroup, - 'CCLAWhitelistRequest': CCLAWhitelistRequest, 'CLAManagerRequest': CLAManagerRequest, - } + if conf["DATABASE"] == "DynamoDB": + return { + "User": User, + "Signature": Signature, + "Repository": Repository, + "Company": Company, + "Project": Project, + "Document": Document, + "GitHubOrg": GitHubOrg, + "Gerrit": Gerrit, + "UserPermissions": UserPermissions, + "Event": Event, + "CompanyInvites": CompanyInvite, + "ProjectCLAGroup": ProjectCLAGroup, + "CCLAWhitelistRequest": CCLAWhitelistRequest, + "CLAManagerRequest": CLAManagerRequest, + } else: - raise Exception('Invalid database selection in configuration: %s' % conf['DATABASE']) + raise Exception("Invalid database selection in configuration: %s" % conf["DATABASE"]) def get_user_instance(conf=None) -> User: @@ -131,7 +146,7 @@ def get_user_instance(conf=None) -> User: :return: A User model instance based on configuration specified. :rtype: cla.models.model_interfaces.User """ - return get_database_models(conf)['User']() + return get_database_models(conf)["User"]() def get_cla_manager_requests_instance(conf=None) -> CLAManagerRequest: @@ -143,7 +158,7 @@ def get_cla_manager_requests_instance(conf=None) -> CLAManagerRequest: :return: A CLAManagerRequest model instance based on configuration specified. :rtype: cla.models.model_interfaces.CLAManagerRequest """ - return get_database_models(conf)['CLAManagerRequest']() + return get_database_models(conf)["CLAManagerRequest"]() def get_user_permissions_instance(conf=None) -> UserPermissions: @@ -155,7 +170,7 @@ def get_user_permissions_instance(conf=None) -> UserPermissions: :return: A UserPermissions model instance based on configuration specified :rtype: cla.models.model_interfaces.UserPermissions """ - return get_database_models(conf)['UserPermissions']() + return get_database_models(conf)["UserPermissions"]() def get_company_invites_instance(conf=None): @@ -167,7 +182,7 @@ def get_company_invites_instance(conf=None): :return: A CompanyInvites model instance based on configuration specified :rtype: cla.models.model_interfaces.CompanyInvite """ - return get_database_models(conf)['CompanyInvites']() + return get_database_models(conf)["CompanyInvites"]() def get_signature_instance(conf=None) -> Signature: @@ -179,7 +194,7 @@ def get_signature_instance(conf=None) -> Signature: :return: An Signature model instance based on configuration. :rtype: cla.models.model_interfaces.Signature """ - return get_database_models(conf)['Signature']() + return get_database_models(conf)["Signature"]() def get_repository_instance(conf=None): @@ -191,7 +206,7 @@ def get_repository_instance(conf=None): :return: A Repository model instance based on configuration specified. :rtype: cla.models.model_interfaces.Repository """ - return get_database_models(conf)['Repository']() + return get_database_models(conf)["Repository"]() def get_github_organization_instance(conf=None): @@ -203,7 +218,7 @@ def get_github_organization_instance(conf=None): :return: A Repository model instance based on configuration specified. :rtype: cla.models.model_interfaces.GitHubOrg """ - return get_database_models(conf)['GitHubOrg']() + return get_database_models(conf)["GitHubOrg"]() def get_gerrit_instance(conf=None): @@ -215,7 +230,7 @@ def get_gerrit_instance(conf=None): :return: A Gerrit model instance based on configuration specified. :rtype: cla.models.model_interfaces.Gerrit """ - return get_database_models(conf)['Gerrit']() + return get_database_models(conf)["Gerrit"]() def get_company_instance(conf=None) -> Company: @@ -227,7 +242,7 @@ def get_company_instance(conf=None) -> Company: :return: A company model instance based on configuration specified. :rtype: cla.models.model_interfaces.Company """ - return get_database_models(conf)['Company']() + return get_database_models(conf)["Company"]() def get_project_instance(conf=None) -> Project: @@ -239,7 +254,7 @@ def get_project_instance(conf=None) -> Project: :return: A Project model instance based on configuration specified. :rtype: cla.models.model_interfaces.Project """ - return get_database_models(conf)['Project']() + return get_database_models(conf)["Project"]() def get_document_instance(conf=None): @@ -251,7 +266,7 @@ def get_document_instance(conf=None): :return: A Document model instance based on configuration specified. :rtype: cla.models.model_interfaces.Document """ - return get_database_models(conf)['Document']() + return get_database_models(conf)["Document"]() def get_event_instance(conf=None) -> Event: @@ -263,7 +278,7 @@ def get_event_instance(conf=None) -> Event: :return: A Event model instance based on configuration :rtype: cla.models.model_interfaces.Event """ - return get_database_models(conf)['Event']() + return get_database_models(conf)["Event"]() def get_project_cla_group_instance(conf=None) -> ProjectCLAGroup: @@ -276,7 +291,7 @@ def get_project_cla_group_instance(conf=None) -> ProjectCLAGroup: :rtype: cla.models.model_interfaces.ProjectCLAGroup """ - return get_database_models(conf)['ProjectCLAGroup']() + return get_database_models(conf)["ProjectCLAGroup"]() def get_ccla_whitelist_request_instance(conf=None) -> CCLAWhitelistRequest: @@ -289,7 +304,7 @@ def get_ccla_whitelist_request_instance(conf=None) -> CCLAWhitelistRequest: :rtype: cla.models.model_interfaces.CCLAWhitelistRequest """ - return get_database_models(conf)['CCLAWhitelistRequest']() + return get_database_models(conf)["CCLAWhitelistRequest"]() def get_email_service(conf=None, initialize=True): @@ -305,19 +320,19 @@ def get_email_service(conf=None, initialize=True): """ if conf is None: conf = cla.conf - email_service = conf['EMAIL_SERVICE'] - if email_service == 'SMTP': + email_service = conf["EMAIL_SERVICE"] + if email_service == "SMTP": from cla.models.smtp_models import SMTP as email - elif email_service == 'MockSMTP': + elif email_service == "MockSMTP": from cla.models.smtp_models import MockSMTP as email - elif email_service == 'SES': + elif email_service == "SES": from cla.models.ses_models import SES as email - elif email_service == 'SNS': + elif email_service == "SNS": from cla.models.sns_email_models import SNS as email - elif email_service == 'MockSES': + elif email_service == "MockSES": from cla.models.ses_models import MockSES as email else: - raise Exception('Invalid email service selected in configuration: %s' % email_service) + raise Exception("Invalid email service selected in configuration: %s" % email_service) email_instance = email() if initialize: email_instance.initialize(conf) @@ -337,13 +352,13 @@ def get_signing_service(conf=None, initialize=True): """ if conf is None: conf = cla.conf - signing_service = conf['SIGNING_SERVICE'] - if signing_service == 'DocuSign': + signing_service = conf["SIGNING_SERVICE"] + if signing_service == "DocuSign": from cla.models.docusign_models import DocuSign as signing - elif signing_service == 'MockDocuSign': + elif signing_service == "MockDocuSign": from cla.models.docusign_models import MockDocuSign as signing else: - raise Exception('Invalid signing service selected in configuration: %s' % signing_service) + raise Exception("Invalid signing service selected in configuration: %s" % signing_service) signing_service_instance = signing() if initialize: signing_service_instance.initialize(conf) @@ -363,15 +378,15 @@ def get_storage_service(conf=None, initialize=True): """ if conf is None: conf = cla.conf - storage_service = conf['STORAGE_SERVICE'] - if storage_service == 'LocalStorage': + storage_service = conf["STORAGE_SERVICE"] + if storage_service == "LocalStorage": from cla.models.local_storage import LocalStorage as storage - elif storage_service == 'S3Storage': + elif storage_service == "S3Storage": from cla.models.s3_storage import S3Storage as storage - elif storage_service == 'MockS3Storage': + elif storage_service == "MockS3Storage": from cla.models.s3_storage import MockS3Storage as storage else: - raise Exception('Invalid storage service selected in configuration: %s' % storage_service) + raise Exception("Invalid storage service selected in configuration: %s" % storage_service) storage_instance = storage() if initialize: storage_instance.initialize(conf) @@ -391,13 +406,13 @@ def get_pdf_service(conf=None, initialize=True): """ if conf is None: conf = cla.conf - pdf_service = conf['PDF_SERVICE'] - if pdf_service == 'DocRaptor': + pdf_service = conf["PDF_SERVICE"] + if pdf_service == "DocRaptor": from cla.models.docraptor_models import DocRaptor as pdf - elif pdf_service == 'MockDocRaptor': + elif pdf_service == "MockDocRaptor": from cla.models.docraptor_models import MockDocRaptor as pdf else: - raise Exception('Invalid PDF service selected in configuration: %s' % pdf_service) + raise Exception("Invalid PDF service selected in configuration: %s" % pdf_service) pdf_instance = pdf() if initialize: pdf_instance.initialize(conf) @@ -415,13 +430,13 @@ def get_key_value_store_service(conf=None): """ if conf is None: conf = cla.conf - keyvalue = cla.conf['KEYVALUE'] - if keyvalue == 'Memory': + keyvalue = cla.conf["KEYVALUE"] + if keyvalue == "Memory": from hug.store import InMemoryStore as Store - elif keyvalue == 'DynamoDB': + elif keyvalue == "DynamoDB": from cla.models.dynamo_models import Store else: - raise Exception('Invalid key-value store selected in configuration: %s' % keyvalue) + raise Exception("Invalid key-value store selected in configuration: %s" % keyvalue) return Store() @@ -438,7 +453,7 @@ def get_supported_repository_providers(): # from cla.models.gitlab_models import GitLab, MockGitLab # return {'github': GitHub, 'mock_github': MockGitHub, # 'gitlab': GitLab, 'mock_gitlab': MockGitLab} - return {'github': GitHub, 'mock_github': MockGitHub} + return {"github": GitHub, "mock_github": MockGitHub} def get_repository_service(provider, initialize=True): @@ -454,7 +469,7 @@ def get_repository_service(provider, initialize=True): """ providers = get_supported_repository_providers() if provider not in providers: - raise NotImplementedError('Provider not supported') + raise NotImplementedError("Provider not supported") instance = providers[provider]() if initialize: instance.initialize(cla.conf) @@ -473,7 +488,7 @@ def get_repository_service_by_repository(repository, initialize=True): :return: A repository provider instance (GitHub, Gerrit, etc). :rtype: RepositoryService """ - repository_model = get_database_models()['Repository'] + repository_model = get_database_models()["Repository"] if isinstance(repository, repository_model): repo = repository else: @@ -490,7 +505,7 @@ def get_supported_document_content_types(): # pylint: disable=invalid-name :return: List of supported document content types. :rtype: dict """ - return ['pdf', 'url+pdf', 'storage+pdf'] + return ["pdf", "url+pdf", "storage+pdf"] def get_project_document(project, document_type, major_version, minor_version): @@ -508,13 +523,12 @@ def get_project_document(project, document_type, major_version, minor_version): :return: The document model if found. :rtype: cla.models.model_interfaces.Document """ - if document_type == 'individual': + if document_type == "individual": documents = project.get_project_individual_documents() else: documents = project.get_project_corporate_documents() for document in documents: - if document.get_document_major_version() == major_version and \ - document.get_document_minor_version() == minor_version: + if document.get_document_major_version() == major_version and document.get_document_minor_version() == minor_version: return document return None @@ -576,48 +590,62 @@ def get_last_version(documents): def user_icla_check(user: User, project: Project, signature: Signature, latest_major_version=False) -> bool: - cla.log.debug(f'ICLA signature found for user: {user} on project: {project}, ' - f'signature_id: {signature.get_signature_id()}') + cla.log.debug( + f"ICLA signature found for user: {user} on project: {project}, " f"signature_id: {signature.get_signature_id()}" + ) # Here's our logic to determine if the signature is valid if latest_major_version: # Ensure it's latest signature. document_models = project.get_project_individual_documents() major, _ = get_last_version(document_models) if signature.get_signature_document_major_version() != major: - cla.log.debug(f'User: {user} only has an old document version signed ' - f'(v{signature.get_signature_document_major_version()}) - needs a new version') + cla.log.debug( + f"User: {user} only has an old document version signed " + f"(v{signature.get_signature_document_major_version()}) - needs a new version" + ) return False if signature.get_signature_signed() and signature.get_signature_approved(): # Signature found and signed/approved. - cla.log.debug(f'User: {user} has ICLA signed and approved signature_id: {signature.get_signature_id()} ' - f'for project: {project}') + cla.log.debug( + f"User: {user} has ICLA signed and approved signature_id: {signature.get_signature_id()} " + f"for project: {project}" + ) return True elif signature.get_signature_signed(): # Not approved yet. - cla.log.debug(f'User: {user} has ICLA signed with signature_id: {signature.get_signature_id()}, ' - f'project: {project}, but has not been approved yet') + cla.log.debug( + f"User: {user} has ICLA signed with signature_id: {signature.get_signature_id()}, " + f"project: {project}, but has not been approved yet" + ) return False else: # Not signed or approved yet. - cla.log.debug(f'User: {user} has ICLA with signature_id: {signature.get_signature_id()}, ' - f'project: {project}, but has not been signed or approved yet') + cla.log.debug( + f"User: {user} has ICLA with signature_id: {signature.get_signature_id()}, " + f"project: {project}, but has not been signed or approved yet" + ) return False def user_ccla_check(user: User, project: Project, signature: Signature) -> bool: - cla.log.debug(f'CCLA signature found for user: {user} on project: {project}, ' - f'signature_id: {signature.get_signature_id()}') + cla.log.debug( + f"CCLA signature found for user: {user} on project: {project}, " f"signature_id: {signature.get_signature_id()}" + ) if signature.get_signature_signed() and signature.get_signature_approved(): - cla.log.debug(f'User: {user} has a signed and approved CCLA for project: {project}') + cla.log.debug(f"User: {user} has a signed and approved CCLA for project: {project}") return True if signature.get_signature_signed(): - cla.log.debug(f'User: {user} has CCLA signed with signature_id: {signature.get_signature_id()}, ' - f'project: {project}, but has not been approved yet') + cla.log.debug( + f"User: {user} has CCLA signed with signature_id: {signature.get_signature_id()}, " + f"project: {project}, but has not been approved yet" + ) return False else: # Not signed or approved yet. - cla.log.debug(f'User: {user} has CCLA with signature_id: {signature.get_signature_id()}, ' - f'project: {project}, but has not been signed or approved yet') + cla.log.debug( + f"User: {user} has CCLA with signature_id: {signature.get_signature_id()}, " + f"project: {project}, but has not been signed or approved yet" + ) return False @@ -635,93 +663,109 @@ def user_signed_project_signature(user: User, project: Project) -> bool: :rtype: boolean """ - fn = 'utils.user_signed_project_signature' + fn = "utils.user_signed_project_signature" # Check if we have an ICLA for this user - cla.log.debug(f'{fn} - checking to see if user has signed an ICLA, user: {user}, project: {project}') + cla.log.debug(f"{fn} - checking to see if user has signed an ICLA, user: {user}, project: {project}") signature = user.get_latest_signature(project.get_project_id(), signature_signed=True, signature_approved=True) icla_pass = False if signature is not None: icla_pass = True else: - cla.log.debug(f'{fn} - ICLA signature NOT found for User: {user} on project: {project}') + cla.log.debug(f"{fn} - ICLA signature NOT found for User: {user} on project: {project}") # If we passed the ICLA check - good, return true, no need to check CCLA if icla_pass: - cla.log.debug( - f'{fn} - ICLA signature check passed for User: {user} on project: {project} - skipping CCLA check') + cla.log.debug(f"{fn} - ICLA signature check passed for User: {user} on project: {project} - skipping CCLA check") return True else: - cla.log.debug( - f'{fn} - ICLA signature check failed for User: {user} on project: {project} - will now check CCLA') + cla.log.debug(f"{fn} - ICLA signature check failed for User: {user} on project: {project} - will now check CCLA") # Check if we have an CCLA for this user company_id = user.get_user_company_id() ccla_pass = False if company_id is not None: - cla.log.debug(f'{fn} - CCLA signature check - user has a company: {company_id} - ' - 'looking up user\'s employee acknowledgement...') + cla.log.debug( + f"{fn} - CCLA signature check - user has a company: {company_id} - " + "looking up user's employee acknowledgement..." + ) # Get employee signature employee_signature = user.get_latest_signature( - project.get_project_id(), - company_id=company_id, - signature_signed=True, - signature_approved=True) + project.get_project_id(), company_id=company_id, signature_signed=True, signature_approved=True + ) if employee_signature is not None: - cla.log.debug(f'{fn} - CCLA signature check - located employee acknowledgement - ' - f'signature id: {employee_signature.get_signature_id()}') + cla.log.debug( + f"{fn} - CCLA signature check - located employee acknowledgement - " + f"signature id: {employee_signature.get_signature_id()}" + ) company = get_company_instance() try: - cla.log.debug(f'{fn} - CCLA signature check - loading company record by id: {company_id}...') + cla.log.debug(f"{fn} - CCLA signature check - loading company record by id: {company_id}...") company.load(company_id) except DoesNotExist as err: - cla.log.debug(f'{fn} - CCLA signature check failed - user is NOT associated with a valid company - ' - f'company with id does not exist: {company_id}.') + cla.log.debug( + f"{fn} - CCLA signature check failed - user is NOT associated with a valid company - " + f"company with id does not exist: {company_id}." + ) return False # Get CCLA signature of company to access whitelist - cla.log.debug(f'{fn} - CCLA signature check - loading signed CCLA for project|company, ' - f'user: {user}, project_id: {project}, company_id: {company_id}') + cla.log.debug( + f"{fn} - CCLA signature check - loading signed CCLA for project|company, " + f"user: {user}, project_id: {project}, company_id: {company_id}" + ) signature = company.get_latest_signature( - project.get_project_id(), signature_signed=True, signature_approved=True) + project.get_project_id(), signature_signed=True, signature_approved=True + ) # Don't check the version for employee signatures. if signature is not None: - cla.log.debug(f'{fn} - CCLA signature check - loaded signed CCLA for project|company, ' - f'user: {user}, project_id: {project}, company_id: {company_id}, ' - f'signature_id: {signature.get_signature_id()}') + cla.log.debug( + f"{fn} - CCLA signature check - loaded signed CCLA for project|company, " + f"user: {user}, project_id: {project}, company_id: {company_id}, " + f"signature_id: {signature.get_signature_id()}" + ) # Verify if user has been approved: https://github.com/communitybridge/easycla/issues/332 - cla.log.debug(f'{fn} - CCLA signature check - ' - 'checking to see if the user is in one of the approval lists...') + cla.log.debug( + f"{fn} - CCLA signature check - " "checking to see if the user is in one of the approval lists..." + ) # if project.get_project_ccla_requires_icla_signature() is True: # cla.log.debug(f'{fn} - CCLA signature check - ' # 'project requires ICLA signature as well as CCLA signature ') - if user.is_approved(signature) : + if user.is_approved(signature): ccla_pass = True else: # Set user signatures approved = false due to user failing whitelist checks - cla.log.debug(f'{fn} - user not in one of the approval lists - ' - 'marking signature approved = false for ' - f'user: {user}, project_id: {project}, company_id: {company_id}') + cla.log.debug( + f"{fn} - user not in one of the approval lists - " + "marking signature approved = false for " + f"user: {user}, project_id: {project}, company_id: {company_id}" + ) user_signatures = user.get_user_signatures( - project_id=project.get_project_id(), company_id=company_id, signature_approved=True, - signature_signed=True + project_id=project.get_project_id(), + company_id=company_id, + signature_approved=True, + signature_signed=True, ) for signature in user_signatures: - cla.log.debug(f'{fn} - user not in one of the approval lists - ' - 'marking signature approved = false for ' - f'user: {user}, project_id: {project}, company_id: {company_id}, ' - f'signature: {signature.get_signature_id()}') + cla.log.debug( + f"{fn} - user not in one of the approval lists - " + "marking signature approved = false for " + f"user: {user}, project_id: {project}, company_id: {company_id}, " + f"signature: {signature.get_signature_id()}" + ) signature.set_signature_approved(False) signature.save() - event_data = (f'The employee signature of user {user.get_user_name()} was ' - f'disapproved the during CCLA check for project {project.get_project_name()} ' - f'and company {company.get_company_name()}') + event_data = ( + f"The employee signature of user {user.get_user_name()} was " + f"disapproved the during CCLA check for project {project.get_project_name()} " + f"and company {company.get_company_name()}" + ) Event.create_event( event_type=EventType.EmployeeSignatureDisapproved, event_cla_group_id=project.get_project_id(), @@ -732,25 +776,31 @@ def user_signed_project_signature(user: User, project: Project) -> bool: contains_pii=True, ) else: - cla.log.debug(f'{fn} - CCLA signature check - unable to load signed CCLA for project|company, ' - f'user: {user}, project_id: {project}, company_id: {company_id} - ' - 'signatory needs to sign the CCLA before the user can be authorized') + cla.log.debug( + f"{fn} - CCLA signature check - unable to load signed CCLA for project|company, " + f"user: {user}, project_id: {project}, company_id: {company_id} - " + "signatory needs to sign the CCLA before the user can be authorized" + ) else: - cla.log.debug(f'{fn} - CCLA signature check - unable to load employee acknowledgement for project|company, ' - f'user: {user}, project_id: {project}, company_id: {company_id}, ' - 'signed=true, approved=true - user needs to be associated with an organization before ' - 'they can be authorized.') + cla.log.debug( + f"{fn} - CCLA signature check - unable to load employee acknowledgement for project|company, " + f"user: {user}, project_id: {project}, company_id: {company_id}, " + "signed=true, approved=true - user needs to be associated with an organization before " + "they can be authorized." + ) else: - cla.log.debug(f'{fn} - CCLA signature check failed - user is NOT associated with a company - ' - f'unable to check for a CCLA, user info: {user}.') + cla.log.debug( + f"{fn} - CCLA signature check failed - user is NOT associated with a company - " + f"unable to check for a CCLA, user info: {user}." + ) if ccla_pass: - cla.log.debug(f'{fn} - CCLA signature check passed for user: {user} on project: {project}') + cla.log.debug(f"{fn} - CCLA signature check passed for user: {user} on project: {project}") return True else: - cla.log.debug(f'{fn} - CCLA signature check failed for user: {user} on project: {project}') + cla.log.debug(f"{fn} - CCLA signature check failed for user: {user} on project: {project}") - cla.log.debug(f'{fn} - User: {user} failed both ICLA and CCLA checks') + cla.log.debug(f"{fn} - User: {user} failed both ICLA and CCLA checks") return False @@ -771,12 +821,13 @@ def get_redirect_uri(repository_service, installation_id, github_repository_id, :return: The redirect_uri parameter expected by the OAuth2 process. :rtype: string """ - params = {'installation_id': installation_id, - 'github_repository_id': github_repository_id, - 'change_request_id': change_request_id} + params = { + "installation_id": installation_id, + "github_repository_id": github_repository_id, + "change_request_id": change_request_id, + } params = urllib.parse.urlencode(params) - return '{}/v2/repository-provider/{}/oauth2_redirect?{}'.format(cla.conf['API_BASE_URL'], repository_service, - params) + return "{}/v2/repository-provider/{}/oauth2_redirect?{}".format(cla.conf["API_BASE_URL"], repository_service, params) def get_full_sign_url(repository_service, installation_id, github_repository_id, change_request_id, project_version): @@ -800,10 +851,9 @@ def get_full_sign_url(repository_service, installation_id, github_repository_id, :type project_version: string """ - base_url = '{}/v2/repository-provider/{}/sign/{}/{}/{}/#/'.format(cla.conf['API_BASE_URL'], repository_service, - str(installation_id), - str(github_repository_id), - str(change_request_id)) + base_url = "{}/v2/repository-provider/{}/sign/{}/{}/{}/#/".format( + cla.conf["API_BASE_URL"], repository_service, str(installation_id), str(github_repository_id), str(change_request_id) + ) return append_project_version_to_url(address=base_url, project_version=project_version) @@ -816,7 +866,7 @@ def append_project_version_to_url(address: str, project_version: str) -> str: :return: returns the final url """ version = "1" - if project_version and project_version == 'v2': + if project_version and project_version == "v2": version = "2" # seem if the url has # in it (https://dev.lfcla.com/#/version=1) the underlying urllib is being confused @@ -839,8 +889,9 @@ def append_project_version_to_url(address: str, project_version: str) -> str: return "?".join([address, query_params_str]) -def get_comment_badge(repository_type, all_signed, sign_url, project_version, missing_user_id=False, - is_approved_by_manager=False): +def get_comment_badge( + repository_type, all_signed, sign_url, project_version, missing_user_id=False, is_approved_by_manager=False +): """ Returns the CLA badge that will appear on the change request comment (PR for 'github', merge request for 'gitlab', etc) @@ -858,40 +909,48 @@ def get_comment_badge(repository_type, all_signed, sign_url, project_version, mi :type is_approved_by_manager: bool """ - alt = 'CLA' + alt = "CLA" if all_signed: - badge_url = f'{CLA_LOGO_URL}/cla-signed.svg' + badge_url = f"{CLA_LOGO_URL}/cla-signed.svg" badge_hyperlink = cla.conf["CLA_LANDING_PAGE"] badge_hyperlink = os.path.join(badge_hyperlink, "#/") badge_hyperlink = append_project_version_to_url(address=badge_hyperlink, project_version=project_version) alt = "CLA Signed" - return (f'' - f'{alt}' - '
') + return ( + f'' + f'{alt}' + "
" + ) else: badge_hyperlink = sign_url - text = '' + text = "" if missing_user_id: - badge_url = f'{CLA_LOGO_URL}/cla-missing-id.svg' - alt = 'CLA Missing ID' - text = (f'{text} ' - f'{alt}' - '') + badge_url = f"{CLA_LOGO_URL}/cla-missing-id.svg" + alt = "CLA Missing ID" + text = ( + f'{text} ' + f'{alt}' + "" + ) if is_approved_by_manager: - badge_url = f'{CLA_LOGO_URL}/cla-confirmation-needed.svg' - alt = 'CLA Confirmation Needed' - text = (f'{text} ' - f'{alt}' - '') + badge_url = f"{CLA_LOGO_URL}/cla-confirmation-needed.svg" + alt = "CLA Confirmation Needed" + text = ( + f'{text} ' + f'{alt}' + "" + ) else: - badge_url = f'{CLA_LOGO_URL}/cla-not-signed.svg' + badge_url = f"{CLA_LOGO_URL}/cla-not-signed.svg" alt = "CLA Not Signed" - text = (f'{text} ' - f'{alt}' - '') + text = ( + f'{text} ' + f'{alt}' + "" + ) - return f'{text}
' + return f"{text}
" def assemble_cla_status(author_name, signed=False): @@ -907,15 +966,21 @@ def assemble_cla_status(author_name, signed=False): :type signed: boolean """ if author_name is None: - author_name = 'Unknown' + author_name = "Unknown" if signed: - return author_name, 'EasyCLA check passed. You are authorized to contribute.' - return author_name, 'Missing CLA Authorization.' + return author_name, "EasyCLA check passed. You are authorized to contribute." + return author_name, "Missing CLA Authorization." -def assemble_cla_comment(repository_type, installation_id, github_repository_id, change_request_id, - signed: List[UserCommitSummary], missing: List[UserCommitSummary], - project_version): +def assemble_cla_comment( + repository_type, + installation_id, + github_repository_id, + change_request_id, + signed: List[UserCommitSummary], + missing: List[UserCommitSummary], + project_version, +): """ Helper function to generate a CLA comment based on a a change request. @@ -950,8 +1015,7 @@ def assemble_cla_comment(repository_type, installation_id, github_repository_id, # Logic not supported as we removed the DB query in the caller # approved_ids = list(filter(lambda x: len(x[1]) == 4 and x[1][3] is True, missing)) # approved_by_manager = len(approved_ids) > 0 - sign_url = get_full_sign_url(repository_type, installation_id, github_repository_id, change_request_id, - project_version) + sign_url = get_full_sign_url(repository_type, installation_id, github_repository_id, change_request_id, project_version) comment = get_comment_body(repository_type, sign_url, signed, missing) all_signed = len(missing) == 0 badge = get_comment_badge( @@ -959,8 +1023,9 @@ def assemble_cla_comment(repository_type, installation_id, github_repository_id, all_signed=all_signed, sign_url=sign_url, project_version=project_version, - missing_user_id=no_user_id) - return badge + '
' + comment + missing_user_id=no_user_id, + ) + return badge + "
" + comment def get_comment_body(repository_type, sign_url, signed: List[UserCommitSummary], missing: List[UserCommitSummary]): @@ -983,11 +1048,11 @@ def get_comment_body(repository_type, sign_url, signed: List[UserCommitSummary], committers_comment = "" num_signed = len(signed) num_missing = len(missing) - text = '' + text = "" # Start of the HTML to render the list of committers if len(signed) > 0 or len(missing) > 0: - committers_comment += '" - committers_comment += '' + committers_comment += "" if len(signed) > 0 and len(missing) == 0: text = "The committers listed above are authorized under a signed CLA." @@ -1099,20 +1173,21 @@ def get_authorization_url_and_state(client_id, redirect_uri, scope, authorize_ur :param authorize_url: The URL to submit the OAuth2 request. :type authorize_url: string """ - fn = 'utils.get_authorization_url_and_state' + fn = "utils.get_authorization_url_and_state" oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope) authorization_url, state = oauth.authorization_url(authorize_url) - cla.log.debug(f'{fn} - initialized a new oauth session ' - f'using the github oauth client id: {client_id[0:5]}... ' - f'with the redirect_uri: {redirect_uri} ' - f'using scope of: {scope}. Obtained the ' - f'state: {state} and the ' - f'generated authorization_url: {authorize_url}') + cla.log.debug( + f"{fn} - initialized a new oauth session " + f"using the github oauth client id: {client_id[0:5]}... " + f"with the redirect_uri: {redirect_uri} " + f"using scope of: {scope}. Obtained the " + f"state: {state} and the " + f"generated authorization_url: {authorize_url}" + ) return authorization_url, state -def fetch_token(client_id, state, token_url, client_secret, code, - redirect_uri=None): # pylint: disable=too-many-arguments +def fetch_token(client_id, state, token_url, client_secret, code, redirect_uri=None): # pylint: disable=too-many-arguments """ Helper function to fetch a OAuth2 session token. @@ -1129,16 +1204,18 @@ def fetch_token(client_id, state, token_url, client_secret, code, :param redirect_uri: The redirect URI for this OAuth2 session. :type redirect_uri: string """ - fn = 'utils.fetch_token' + fn = "utils.fetch_token" if redirect_uri is not None: - oauth2 = OAuth2Session(client_id, state=state, scope=['user:email'], redirect_uri=redirect_uri) + oauth2 = OAuth2Session(client_id, state=state, scope=["user:email"], redirect_uri=redirect_uri) else: - oauth2 = OAuth2Session(client_id, state=state, scope=['user:email']) - cla.log.debug(f'{fn} - oauth2.fetch_token - ' - f'token_url: {token_url}, ' - f'client_id: {client_id}, ' - f'client_secret: {client_secret}, ' - f'code: {code}') + oauth2 = OAuth2Session(client_id, state=state, scope=["user:email"]) + cla.log.debug( + f"{fn} - oauth2.fetch_token - " + f"token_url: {token_url}, " + f"client_id: {client_id}, " + f"client_secret: {client_secret}, " + f"code: {code}" + ) return oauth2.fetch_token(token_url, client_secret=client_secret, code=code) @@ -1155,30 +1232,30 @@ def redirect_user_by_signature(user, signature): if signature.get_signature_signed() and signature.get_signature_approved(): # Signature already signed and approved. # TODO: Notify user of signed and approved signature somehow. - cla.log.info('Signature already signed and approved for user: %s, %s', - user.get_user_emails(), signature.get_signature_id()) + cla.log.info( + "Signature already signed and approved for user: %s, %s", user.get_user_emails(), signature.get_signature_id() + ) if return_url is None: - cla.log.info('No return_url set in signature object - serving success message') - return {'status': 'signed and approved'} + cla.log.info("No return_url set in signature object - serving success message") + return {"status": "signed and approved"} else: - cla.log.info('Redirecting user back to %s', return_url) + cla.log.info("Redirecting user back to %s", return_url) raise falcon.HTTPFound(return_url) elif signature.get_signature_signed(): # Awaiting approval. # TODO: Notify user of pending approval somehow. - cla.log.info('Signature signed but not approved yet: %s', - signature.get_signature_id()) + cla.log.info("Signature signed but not approved yet: %s", signature.get_signature_id()) if return_url is None: - cla.log.info('No return_url set in signature object - serving pending message') - return {'status': 'pending approval'} + cla.log.info("No return_url set in signature object - serving pending message") + return {"status": "pending approval"} else: - cla.log.info('Redirecting user back to %s', return_url) + cla.log.info("Redirecting user back to %s", return_url) raise falcon.HTTPFound(return_url) else: # Signature awaiting signature. sign_url = signature.get_signature_sign_url() signature_id = signature.get_signature_id() - cla.log.info('Signature exists, sending user to sign: %s (%s)', signature_id, sign_url) + cla.log.info("Signature exists, sending user to sign: %s (%s)", signature_id, sign_url) raise falcon.HTTPFound(sign_url) @@ -1195,7 +1272,7 @@ def get_active_signature_metadata(user_id): :rtype: dict """ store = get_key_value_store_service() - key = 'active_signature:' + str(user_id) + key = "active_signature:" + str(user_id) if store.exists(key): return json.loads(store.get(key)) return None @@ -1218,13 +1295,12 @@ def set_active_signature_metadata(user_id, project_id, repository_id, pull_reque :type pull_request_id: string """ store = get_key_value_store_service() - key = 'active_signature:' + str(user_id) # Should have been set when user initiated the signature. - value = json.dumps({'user_id': user_id, - 'project_id': project_id, - 'repository_id': repository_id, - 'pull_request_id': pull_request_id}) + key = "active_signature:" + str(user_id) # Should have been set when user initiated the signature. + value = json.dumps( + {"user_id": user_id, "project_id": project_id, "repository_id": repository_id, "pull_request_id": pull_request_id} + ) store.set(key, value) - cla.log.info('Stored active signature details for user %s: Key - %s Value - %s', user_id, key, value) + cla.log.info("Stored active signature details for user %s: Key - %s Value - %s", user_id, key, value) def delete_active_signature_metadata(user_id): @@ -1235,13 +1311,14 @@ def delete_active_signature_metadata(user_id): :type user_id: string """ store = get_key_value_store_service() - key = 'active_signature:' + str(user_id) + key = "active_signature:" + str(user_id) store.delete(key) - cla.log.info('Deleted stored active signature details for user %s', user_id) + cla.log.info("Deleted stored active signature details for user %s", user_id) -def set_active_pr_metadata(github_author_username: str, github_author_email: str, - cla_group_id: str, repository_id: str, pull_request_id: str): +def set_active_pr_metadata( + github_author_username: str, github_author_email: str, cla_group_id: str, repository_id: str, pull_request_id: str +): """ When we receive a GitHub PR callback, we want to store a bit if information/metadata about the repository, PR, commit authors, and associated CLA Group so that we can later @@ -1265,22 +1342,22 @@ def set_active_pr_metadata(github_author_username: str, github_author_email: str # the same value is stored twice, indexed separately by username and email to allow lookups by either value = json.dumps( { - 'github_author_username': github_author_username, - 'github_author_email': github_author_email, - 'cla_group_id': cla_group_id, - 'repository_id': repository_id, - 'pull_request_id': pull_request_id + "github_author_username": github_author_username, + "github_author_email": github_author_email, + "cla_group_id": cla_group_id, + "repository_id": repository_id, + "pull_request_id": pull_request_id, } ) - key_github_author_username = 'active_pr:u:' + github_author_username + key_github_author_username = "active_pr:u:" + github_author_username store.set(key_github_author_username, value) - cla.log.info(f'stored active pull request details by user email: %s', key_github_author_username) + cla.log.info(f"stored active pull request details by user email: %s", key_github_author_username) if github_author_email is not None: - key_github_author_email = 'active_pr:e:' + github_author_email + key_github_author_email = "active_pr:e:" + github_author_email store.set(key_github_author_email, value) - cla.log.info(f'stored active pull request details by user email: %s', key_github_author_email) + cla.log.info(f"stored active pull request details by user email: %s", key_github_author_email) def get_active_signature_return_url(user_id, metadata=None): @@ -1297,33 +1374,30 @@ def get_active_signature_return_url(user_id, metadata=None): if metadata is None: metadata = get_active_signature_metadata(user_id) if metadata is None: - cla.log.warning('Could not find active signature for user {}, return URL request failed'.format(user_id)) + cla.log.warning("Could not find active signature for user {}, return URL request failed".format(user_id)) return None # Factor in Gitlab flow process if "merge_request_id" in metadata.keys(): - return metadata['return_url'] + return metadata["return_url"] # Get Github ID from metadata - github_repository_id = metadata['repository_id'] + github_repository_id = metadata["repository_id"] # Get installation id through a helper function installation_id = get_installation_id_from_github_repository(github_repository_id) if installation_id is None: - cla.log.error('Could not find installation ID that is configured for this repository ID: %s', - github_repository_id) + cla.log.error("Could not find installation ID that is configured for this repository ID: %s", github_repository_id) return None - github = cla.utils.get_repository_service('github') - return github.get_return_url(metadata['repository_id'], - metadata['pull_request_id'], - installation_id) + github = cla.utils.get_repository_service("github") + return github.get_return_url(metadata["repository_id"], metadata["pull_request_id"], installation_id) def get_installation_id_from_github_repository(github_repository_id): # Get repository ID that references the github ID. try: - repository = Repository().get_repository_by_external_id(github_repository_id, 'github') + repository = Repository().get_repository_by_external_id(github_repository_id, "github") except DoesNotExist: return None @@ -1341,7 +1415,7 @@ def get_installation_id_from_github_repository(github_repository_id): def get_organization_id_from_gitlab_repository(gitlab_repository_id): # Get repository ID that references the gitlab ID. try: - repository = Repository().get_repository_by_external_id(gitlab_repository_id, 'gitlab') + repository = Repository().get_repository_by_external_id(gitlab_repository_id, "gitlab") except DoesNotExist: return None # Get GitLabGroup from this repository @@ -1359,7 +1433,7 @@ def get_organization_id_from_gitlab_repository(gitlab_repository_id): def get_project_id_from_github_repository(github_repository_id): # Get repository ID that references the github ID. try: - repository = Repository().get_repository_by_external_id(github_repository_id, 'github') + repository = Repository().get_repository_by_external_id(github_repository_id, "github") except DoesNotExist: return None @@ -1381,21 +1455,25 @@ def get_individual_signature_callback_url(user_id, metadata=None): if metadata is None: metadata = get_active_signature_metadata(user_id) if metadata is None: - cla.log.warning('Could not find active signature for user {}, callback URL request failed'.format(user_id)) + cla.log.warning("Could not find active signature for user {}, callback URL request failed".format(user_id)) return None # Get Github ID from metadata - github_repository_id = metadata['repository_id'] + github_repository_id = metadata["repository_id"] # Get installation id through a helper function installation_id = get_installation_id_from_github_repository(github_repository_id) if installation_id is None: - cla.log.error('Could not find installation ID that is configured for this repository ID: %s', - github_repository_id) + cla.log.error("Could not find installation ID that is configured for this repository ID: %s", github_repository_id) return None - return os.path.join(API_BASE_URL, 'v2/signed/individual', str(installation_id), str(metadata['repository_id']), - str(metadata['pull_request_id'])) + return os.path.join( + API_BASE_URL, + "v2/signed/individual", + str(installation_id), + str(metadata["repository_id"]), + str(metadata["pull_request_id"]), + ) def get_individual_signature_callback_url_gitlab(user_id, metadata=None): @@ -1412,23 +1490,29 @@ def get_individual_signature_callback_url_gitlab(user_id, metadata=None): if metadata is None: metadata = get_active_signature_metadata(user_id) if metadata is None: - cla.log.warning('Could not find active signature for user {}, callback URL request failed'.format(user_id)) + cla.log.warning("Could not find active signature for user {}, callback URL request failed".format(user_id)) return None # Get GitLab ID from metadata - gitlab_repository_id = metadata['repository_id'] + gitlab_repository_id = metadata["repository_id"] # Get organization id organization_id = get_organization_id_from_gitlab_repository(gitlab_repository_id) if organization_id is None: - cla.log.error('Could not find GitLab organization ID that is configured for this repository ID: %s', - gitlab_repository_id) + cla.log.error( + "Could not find GitLab organization ID that is configured for this repository ID: %s", gitlab_repository_id + ) return None - return os.path.join(API_BASE_URL, 'v2/signed/gitlab/individual', str(user_id), str(organization_id), - str(metadata['repository_id']), - str(metadata['merge_request_id'])) + return os.path.join( + API_BASE_URL, + "v2/signed/gitlab/individual", + str(user_id), + str(organization_id), + str(metadata["repository_id"]), + str(metadata["merge_request_id"]), + ) def request_individual_signature(installation_id, github_repository_id, user, change_request_id, callback_url=None): @@ -1450,24 +1534,19 @@ def request_individual_signature(installation_id, github_repository_id, user, ch :type callback_url: string """ project_id = get_project_id_from_github_repository(github_repository_id) - repo_service = get_repository_service('github') - return_url = repo_service.get_return_url(github_repository_id, - change_request_id, - installation_id) + repo_service = get_repository_service("github") + return_url = repo_service.get_return_url(github_repository_id, change_request_id, installation_id) if callback_url is None: - callback_url = os.path.join(API_BASE_URL, 'v2/signed/individual', str(installation_id), str(change_request_id)) + callback_url = os.path.join(API_BASE_URL, "v2/signed/individual", str(installation_id), str(change_request_id)) signing_service = get_signing_service() - return_url_type = 'Github' - signature_data = signing_service.request_individual_signature(project_id, - user.get_user_id(), - return_url_type, - return_url, - callback_url) - if 'sign_url' in signature_data: - raise falcon.HTTPFound(signature_data['sign_url']) - cla.log.error('Could not get sign_url from signing service provider - sending user ' - 'to return_url instead') + return_url_type = "Github" + signature_data = signing_service.request_individual_signature( + project_id, user.get_user_id(), return_url_type, return_url, callback_url + ) + if "sign_url" in signature_data: + raise falcon.HTTPFound(signature_data["sign_url"]) + cla.log.error("Could not get sign_url from signing service provider - sending user " "to return_url instead") raise falcon.HTTPFound(return_url) @@ -1478,19 +1557,18 @@ def lookup_user_gitlab_username(user_gitlab_id: int) -> Optional[str]: :return: the user's gitlab login/username """ try: - r = requests.get(f'https://gitlab.com/api/v4/users/{user_gitlab_id}') + r = requests.get(f"https://gitlab.com/api/v4/users/{user_gitlab_id}") r.raise_for_status() except requests.exceptions.HTTPError as err: - msg = f'Could not get user github user from id: {user_gitlab_id}: error: {err}' + msg = f"Could not get user github user from id: {user_gitlab_id}: error: {err}" cla.log.warning(msg) return None gitlab_user = r.json() - if 'id' in gitlab_user: - return gitlab_user['id'] + if "id" in gitlab_user: + return gitlab_user["id"] else: - cla.log.warning('Malformed HTTP response from GitLab - expecting "id" attribute ' - f'- response: {gitlab_user}') + cla.log.warning('Malformed HTTP response from GitLab - expecting "id" attribute ' f"- response: {gitlab_user}") return None @@ -1501,19 +1579,18 @@ def lookup_user_gitlab_id(user_gitlab_username: str) -> Optional[str]: :return: the user's gitlab id """ try: - r = requests.get(f'https://gitlab.com/api/v4/users?username={user_gitlab_username}') + r = requests.get(f"https://gitlab.com/api/v4/users?username={user_gitlab_username}") r.raise_for_status() except requests.exceptions.HTTPError as err: - msg = f'Could not get user github user from username: {user_gitlab_username}: error: {err}' + msg = f"Could not get user github user from username: {user_gitlab_username}: error: {err}" cla.log.warning(msg) return None gitlab_user = r.json() - if 'username' in gitlab_user: - return gitlab_user['username'] + if "username" in gitlab_user: + return gitlab_user["username"] else: - cla.log.warning('Malformed HTTP response from GitLab - expecting "username" attribute ' - f'- response: {gitlab_user}') + cla.log.warning('Malformed HTTP response from GitLab - expecting "username" attribute ' f"- response: {gitlab_user}") return None @@ -1525,28 +1602,28 @@ def lookup_user_github_username(user_github_id: int) -> Optional[str]: """ try: headers = { - 'Authorization': 'Bearer {}'.format(cla.conf['GITHUB_OAUTH_TOKEN']), - 'Accept': 'application/json', + "Authorization": "Bearer {}".format(cla.conf["GITHUB_OAUTH_TOKEN"]), + "Accept": "application/json", } - r = requests.get(f'https://api.github.com/user/{user_github_id}', headers=headers) + r = requests.get(f"https://api.github.com/user/{user_github_id}", headers=headers) r.raise_for_status() except requests.exceptions.HTTPError as err: - msg = f'Could not get user github user from id: {user_github_id}: error: {err}' + msg = f"Could not get user github user from id: {user_github_id}: error: {err}" cla.log.warning(msg) return None github_user = r.json() - if 'message' in github_user: - cla.log.warning(f'Unable to lookup user from id: {user_github_id} ' - f'- message: {github_user["message"]}') + if "message" in github_user: + cla.log.warning(f"Unable to lookup user from id: {user_github_id} " f'- message: {github_user["message"]}') return None else: - if 'login' in github_user: - return github_user['login'] + if "login" in github_user: + return github_user["login"] else: - cla.log.warning('Malformed HTTP response from GitHub - expecting "login" attribute ' - f'- response: {github_user}') + cla.log.warning( + 'Malformed HTTP response from GitHub - expecting "login" attribute ' f"- response: {github_user}" + ) return None @@ -1558,28 +1635,26 @@ def lookup_user_github_id(user_github_username: str) -> Optional[int]: """ try: headers = { - 'Authorization': 'Bearer {}'.format(cla.conf['GITHUB_OAUTH_TOKEN']), - 'Accept': 'application/json', + "Authorization": "Bearer {}".format(cla.conf["GITHUB_OAUTH_TOKEN"]), + "Accept": "application/json", } - r = requests.get(f'https://api.github.com/users/{user_github_username}', headers=headers) + r = requests.get(f"https://api.github.com/users/{user_github_username}", headers=headers) r.raise_for_status() except requests.exceptions.HTTPError as err: - msg = f'Could not get user github id from username: {user_github_username}: error: {err}' + msg = f"Could not get user github id from username: {user_github_username}: error: {err}" cla.log.warning(msg) return None github_user = r.json() - if 'message' in github_user: - cla.log.warning(f'Unable to lookup user from id: {user_github_username} ' - f'- message: {github_user["message"]}') + if "message" in github_user: + cla.log.warning(f"Unable to lookup user from id: {user_github_username} " f'- message: {github_user["message"]}') return None else: - if 'id' in github_user: - return github_user['id'] + if "id" in github_user: + return github_user["id"] else: - cla.log.warning('Malformed HTTP response from GitHub - expecting "id" attribute ' - f'- response: {github_user}') + cla.log.warning('Malformed HTTP response from GitHub - expecting "id" attribute ' f"- response: {github_user}") return None @@ -1587,27 +1662,27 @@ def lookup_github_organizations(github_username: str): # Use the Github API to retrieve github orgs that the user is a member of (user must be a public member). try: headers = { - 'Authorization': 'Bearer {}'.format(cla.conf['GITHUB_OAUTH_TOKEN']), - 'Accept': 'application/json', + "Authorization": "Bearer {}".format(cla.conf["GITHUB_OAUTH_TOKEN"]), + "Accept": "application/json", } - r = requests.get(f'https://api.github.com/users/{github_username}/orgs', headers=headers) + r = requests.get(f"https://api.github.com/users/{github_username}/orgs", headers=headers) r.raise_for_status() except requests.exceptions.HTTPError as err: - cla.log.warning('Could not get user github org: {}'.format(err)) - return {'error': 'Could not get user github org: {}'.format(err)} - return [github_org['login'] for github_org in r.json()] + cla.log.warning("Could not get user github org: {}".format(err)) + return {"error": "Could not get user github org: {}".format(err)} + return [github_org["login"] for github_org in r.json()] def lookup_gitlab_org_members(organization_id): # Use the v2 Endpoint thats a wrapper for Gitlab Group member query try: - r = requests.get(f'{cla.config.PLATFORM_GATEWAY_URL}/cla-service/v4/gitlab/group/{organization_id}/members') + r = requests.get(f"{cla.config.PLATFORM_GATEWAY_URL}/cla-service/v4/gitlab/group/{organization_id}/members") r.raise_for_status() except requests.exceptions.HTTPError as err: - cla.log.warning(f'Could not fetch gitlab org users: {err}') - return {f'error: Could not get user gitlab group id: {organization_id} members: {err}'} - return r.json()['list'] + cla.log.warning(f"Could not fetch gitlab org users: {err}") + return {f"error: Could not get user gitlab group id: {organization_id} members: {err}"} + return r.json()["list"] def update_github_username(github_user: dict, user: User): @@ -1619,16 +1694,18 @@ def update_github_username(github_user: dict, user: User): :return: None """ # set the github username if available - if 'login' in github_user: + if "login" in github_user: if user.get_user_github_username() is None: cla.log.debug(f'Updating user record - adding github username: {github_user["login"]}') - user.set_user_github_username(github_user['login']) - if user.get_user_github_username() != github_user['login']: - cla.log.warning(f'Note: github user with id: {github_user["id"]}' - f' has a mismatched username (gh: {github_user["id"]} ' - f'vs db user record: {user.get_user_github_username}) - ' - f'setting the value to: {github_user["login"]}') - user.set_user_github_username(github_user['login']) + user.set_user_github_username(github_user["login"]) + if user.get_user_github_username() != github_user["login"]: + cla.log.warning( + f'Note: github user with id: {github_user["id"]}' + f' has a mismatched username (gh: {github_user["id"]} ' + f"vs db user record: {user.get_user_github_username}) - " + f'setting the value to: {github_user["login"]}' + ) + user.set_user_github_username(github_user["login"]) def is_approved(ccla_signature: Signature, email=None, github_username=None, github_id=None): @@ -1642,28 +1719,29 @@ def is_approved(ccla_signature: Signature, email=None, github_username=None, git :param github_username: A given github username checked against ccla signature github/github-org whitelists :param github_id: A given github id checked against ccla signature github/github-org whitelists """ - fn = 'utils.is_approved' + fn = "utils.is_approved" if email: # Checking email whitelist whitelist = ccla_signature.get_email_whitelist() - cla.log.debug(f'{fn} - testing email: {email} with CCLA approval list emails: {whitelist}') + cla.log.debug(f"{fn} - testing email: {email} with CCLA approval list emails: {whitelist}") if whitelist is not None: if email.lower() in (s.lower() for s in whitelist): - cla.log.debug(f'{fn} found user email in email approval list') + cla.log.debug(f"{fn} found user email in email approval list") return True # Checking domain whitelist patterns = ccla_signature.get_domain_whitelist() - cla.log.debug(f"{fn} - testing user email domain: {email} with " - f"domain approval list values in database: {patterns}") + cla.log.debug( + f"{fn} - testing user email domain: {email} with " f"domain approval list values in database: {patterns}" + ) if patterns is not None: if get_user_instance().preprocess_pattern([email], patterns): return True else: cla.log.debug(f"{fn} - did not match email: {email} with domain: {patterns}") else: - cla.log.debug(f'{fn} - no domain approval list patterns defined - skipping domain approval list check') + cla.log.debug(f"{fn} - no domain approval list patterns defined - skipping domain approval list check") if github_id: github_username = lookup_user_github_username(github_id) @@ -1673,16 +1751,18 @@ def is_approved(ccla_signature: Signature, email=None, github_username=None, git # remove leading and trailing whitespace from github username github_username = github_username.strip() github_approval_list = ccla_signature.get_github_whitelist() - cla.log.debug(f"{fn} - testing user github username: {github_username} with " - f"CCLA github approval list: {github_approval_list}") + cla.log.debug( + f"{fn} - testing user github username: {github_username} with " + f"CCLA github approval list: {github_approval_list}" + ) if github_approval_list is not None: # case insensitive search if github_username.lower() in (s.lower() for s in github_approval_list): - cla.log.debug(f'{fn} - found github username in github approval list') + cla.log.debug(f"{fn} - found github username in github approval list") return True else: - cla.log.debug(f'{fn} - users github_username is not defined - skipping github username approval list check') + cla.log.debug(f"{fn} - users github_username is not defined - skipping github username approval list check") # Check github org approval list if github_username is not None: @@ -1690,29 +1770,31 @@ def is_approved(ccla_signature: Signature, email=None, github_username=None, git if "error" not in github_orgs: # Fetch the list of orgs this user is part of github_org_approval_list = ccla_signature.get_github_org_whitelist() - cla.log.debug(f'{fn} - testing user github orgs: {github_orgs} with ' - f'CCLA github org approval list values: {github_org_approval_list}') + cla.log.debug( + f"{fn} - testing user github orgs: {github_orgs} with " + f"CCLA github org approval list values: {github_org_approval_list}" + ) if github_org_approval_list is not None: for dynamo_github_org in github_org_approval_list: # case insensitive search if dynamo_github_org.lower() in (s.lower() for s in github_orgs): - cla.log.debug(f'{fn} - found matching github org for user') + cla.log.debug(f"{fn} - found matching github org for user") return True else: - cla.log.debug(f'{fn} - users github_username is not defined - skipping github org approval list check') + cla.log.debug(f"{fn} - users github_username is not defined - skipping github org approval list check") - cla.log.debug(f'{fn} - unable to find user in any approval list') + cla.log.debug(f"{fn} - unable to find user in any approval list") return False def audit_event(func): - """ Decorator that audits events """ + """Decorator that audits events""" def wrapper(**kwargs): response = func(**kwargs) if response.get("status_code") == falcon.HTTP_200: - cla.log.debug("Created event {} ".format(kwargs['event_type'])) + cla.log.debug("Created event {} ".format(kwargs["event_type"])) else: cla.log.debug("Failed to add event") return response @@ -1721,7 +1803,7 @@ def wrapper(**kwargs): def get_oauth_client(): - return OAuth2Session(os.environ['GH_OAUTH_CLIENT_ID']) + return OAuth2Session(os.environ["GH_OAUTH_CLIENT_ID"]) def fmt_project(project: Project): @@ -1729,39 +1811,33 @@ def fmt_project(project: Project): def fmt_company(company: Company): - return "{} ({}) - acl: {}".format( - company.get_company_name(), - company.get_company_id(), - company.get_company_acl()) + return "{} ({}) - acl: {}".format(company.get_company_name(), company.get_company_id(), company.get_company_acl()) def fmt_user(user: User): - return '{} ({}) {}'.format( - user.get_user_name(), - user.get_user_id(), - user.get_lf_email()) + return "{} ({}) {}".format(user.get_user_name(), user.get_user_id(), user.get_lf_email()) def fmt_users(users: List[User]): - response = '' + response = "" for user in users: - response += fmt_user(user) + ' ' + response += fmt_user(user) + " " return response def get_email_help_content(show_v2_help_link: bool) -> str: # v1 help link - help_link = 'https://docs.linuxfoundation.org/lfx/easycla' + help_link = "https://docs.linuxfoundation.org/lfx/easycla" if show_v2_help_link: # v2 help link - help_link = 'https://docs.linuxfoundation.org/lfx/easycla' + help_link = "https://docs.linuxfoundation.org/lfx/easycla" return f'

If you need help or have questions about EasyCLA, you can read the documentation or reach out to us for support.

' def get_email_sign_off_content() -> str: - return '

Thanks,

EasyCLA Support Team

' + return "

Thanks,

EasyCLA Support Team

" def get_corporate_url(project_version: str) -> str: @@ -1770,7 +1846,7 @@ def get_corporate_url(project_version: str) -> str: :param project_version: cla_group version(v1|v2) :return: default is v1 corporate console """ - return CORPORATE_V2_BASE if project_version == 'v2' else CORPORATE_BASE + return CORPORATE_V2_BASE if project_version == "v2" else CORPORATE_BASE def append_email_help_sign_off_content(body: str, project_version: str) -> str: @@ -1780,11 +1856,13 @@ def append_email_help_sign_off_content(body: str, project_version: str) -> str: :param project_version: :return: """ - return "".join([ - body, - get_email_help_content(project_version == "v2"), - get_email_sign_off_content(), - ]) + return "".join( + [ + body, + get_email_help_content(project_version == "v2"), + get_email_sign_off_content(), + ] + ) def append_email_help_sign_off_content_plain(body: str, project_version: str) -> str: @@ -1820,7 +1898,7 @@ def get_time_from_string(date_string: str) -> Optional[datetime]: :return: """ # Try these formats - formats = ['%Y-%m-%d %H:%M:%S.%f%z', '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S.%f%z', '%Y-%m-%dT%H:%M:%S.%f'] + formats = ["%Y-%m-%d %H:%M:%S.%f%z", "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z", "%Y-%m-%dT%H:%M:%S.%f"] for fmt in formats: try: return datetime.strptime(date_string, fmt) @@ -1837,20 +1915,21 @@ def get_public_email(user): if len(user.get_all_user_emails()) > 0: return next((email for email in user.get_all_user_emails() if "noreply.github.com" not in email), None) + def get_co_authors_from_commit(commit): """ Helper function to return co-authors from commit """ fn = "get_co_authors_from_commit" - # import pdb; pdb.set_trace() co_authors = [] if commit.commit: commit_message = commit.commit.message - cla.log.debug(f'{fn} - commit message: {commit_message}') + cla.log.debug(f"{fn} - commit message: {commit_message}") if commit_message: co_authors = re.findall(r"Co-authored-by: (.*) <(.*)>", commit_message) return co_authors + def extract_pull_request_number(pull_request_message): """ Helper function to return pull request number from pull request message @@ -1862,11 +1941,7 @@ def extract_pull_request_number(pull_request_message): try: pull_request_number = int(re.search(r"#(\d+)", pull_request_message).group(1)) except AttributeError as e: - cla.log.warning(f'{fn} - unable to extract pull request number from message: {pull_request_message}, error: {e}') + cla.log.warning(f"{fn} - unable to extract pull request number from message: {pull_request_message}, error: {e}") except Exception as e: - cla.log.warning(f'{fn} - unable to extract pull request number from message: {pull_request_message}, error: {e}') + cla.log.warning(f"{fn} - unable to extract pull request number from message: {pull_request_message}, error: {e}") return pull_request_number - - - -