class Whitelist: def __init__(self, fas_user: str = None, fas_password: str = None): self.db = PersistentDict(hash_name="whitelist") self._fas: AccountSystem = AccountSystem(username=fas_user, password=fas_password) def _signed_fpca(self, account_login: str) -> bool: """ Check if the user is a packager, by checking if their GitHub username is in the 'packager' group in FAS. Works only the user's username is the same in GitHub and FAS. :param account_login: str, Github username :return: bool """ try: person = self._fas.person_by_username(account_login) except AuthError as e: logger.error(f"FAS authentication failed: {e!r}") return False except FedoraServiceError as e: logger.error(f"FAS query failed: {e!r}") return False if not person: logger.info(f"Not a FAS username {account_login!r}.") return False for membership in person.get("memberships", []): if membership.get("name") == "cla_fpca": logger.info(f"User {account_login!r} signed FPCA!") return True logger.info(f"Cannot verify whether {account_login!r} signed FPCA.") return False def get_account(self, account_name: str) -> Optional[dict]: """ Get selected account from DB, return None if it's not there :param account_name: account name for approval """ account = self.db.get(account_name) if not account: return None # patch status db_status = account["status"] if db_status.startswith("WhitelistStatus"): account["status"] = db_status.split(".", 1)[1] self.db[account_name] = account return account def add_account(self, github_app: InstallationEvent) -> bool: """ Add account to whitelist. Status is set to 'waiting' or to 'approved_automatically' if the account is a packager in Fedora. :param github_app: github app installation info :return: was the account (auto/already)-whitelisted? """ if github_app.account_login in self.db: # TODO: if the sender added (not created) our App to more repos, # then we should update the DB here return True # Do the DB insertion as a first thing to avoid issue#42 github_app.status = WhitelistStatus.waiting self.db[github_app.account_login] = github_app.get_dict() # we want to verify if user who installed the application (sender_login) signed FPCA # https://fedoraproject.org/wiki/Legal:Fedora_Project_Contributor_Agreement if self._signed_fpca(github_app.sender_login): github_app.status = WhitelistStatus.approved_automatically self.db[github_app.account_login] = github_app.get_dict() return True else: return False def approve_account(self, account_name: str) -> bool: """ Approve user manually :param account_name: account name for approval :return: """ account = self.get_account(account_name) or {} account["status"] = WhitelistStatus.approved_manually.value self.db[account_name] = account logger.info(f"Account {account_name} approved successfully") return True def is_approved(self, account_name: str) -> bool: """ Check if user is approved in the whitelist :param account_name: :return: """ if account_name in self.db: account = self.get_account(account_name) db_status = account["status"] s = WhitelistStatus(db_status) return (s == WhitelistStatus.approved_automatically or s == WhitelistStatus.approved_manually) return False def remove_account(self, account_name: str) -> bool: """ Remove account from whitelist. :param account_name: github login :return: """ if account_name in self.db: del self.db[account_name] # TODO: delete all artifacts from copr logger.info(f"Account: {account_name} removed from whitelist!") return True else: logger.info(f"Account: {account_name} does not exists!") return False def accounts_waiting(self) -> list: """ Get accounts waiting for approval :return: list of accounts waiting for approval """ return [ key for (key, item) in self.db.items() if WhitelistStatus(item["status"]) == WhitelistStatus.waiting ] def check_and_report(self, event: Optional[Any], project: GitProject, config: ServiceConfig) -> bool: """ Check if account is approved and report status back in case of PR :param config: service config :param event: PullRequest and Release TODO: handle more :param project: GitProject :return: """ # TODO: modify event hierarchy so we can use some abstract classes instead if isinstance(event, ReleaseEvent): account_name = event.repo_namespace if not account_name: raise KeyError( f"Failed to get account_name from {type(event)}") if not self.is_approved(account_name): logger.info( f"Refusing release event on not whitelisted repo namespace" ) return False return True if isinstance( event, (CoprBuildEvent, TestingFarmResultsEvent, DistGitEvent, InstallationEvent), ): return True if isinstance(event, (PullRequestEvent, PullRequestCommentEvent)): account_name = event.github_login if not account_name: raise KeyError( f"Failed to get account_name from {type(event)}") namespace = event.base_repo_namespace # FIXME: # Why check account_name when we whitelist namespace only (in whitelist.add_account())? if not (self.is_approved(account_name) or self.is_approved(namespace)): msg = f"Neither account {account_name} nor owner {namespace} are on our whitelist!" logger.error(msg) # TODO also check blacklist, # but for that we need to know who triggered the action if event.trigger == JobTriggerType.comment: project.pr_comment(event.pr_id, msg) else: job_helper = CoprBuildJobHelper( config=config, package_config=event.get_package_config(), project=project, event=event, ) msg = "Account is not whitelisted!" # needs to be shorter job_helper.report_status_to_all(description=msg, state="error", url=FAQ_URL) return False # TODO: clear failing check when present return True if isinstance(event, IssueCommentEvent): account_name = event.github_login if not account_name: raise KeyError( f"Failed to get account_name from {type(event)}") namespace = event.base_repo_namespace # FIXME: # Why check account_name when we whitelist namespace only (in whitelist.add_account())? if not (self.is_approved(account_name) or self.is_approved(namespace)): msg = f"Neither account {account_name} nor owner {namespace} are on our whitelist!" logger.error(msg) project.issue_comment(event.issue_id, msg) # TODO also check blacklist, # but for that we need to know who triggered the action return False return True msg = f"Failed to validate account: Unrecognized event type {type(event)}." logger.error(msg) raise PackitException(msg)
def get(self, id): """A specific copr build details. From copr_build hash, filled by worker.""" # hash name is defined in worker (CoprBuildDB), which I don't want to import from db = PersistentDict(hash_name="copr_build") build = db.get(id) return build if build else ("", HTTPStatus.NO_CONTENT)