Пример #1
0
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)
Пример #2
0
class Whitelist:
    def __init__(self):
        self.db = PersistentDict(hash_name="whitelist")

    @staticmethod
    def _is_packager(account_login: str) -> bool:
        """
        If GitHub username is same as FAS username this method checks if user is packager.
        User is considered to be packager when he/she has the badge:
         `If you build it... (Koji Success I)`
        :param account_login: str, Github username
        :return: bool
        """

        url = f"https://badges.fedoraproject.org/user/{account_login}/json"
        data = requests.get(url)
        if not data:
            return False
        assertions = data.json().get("assertions")
        if not assertions:
            return False
        for item in assertions:
            if "Succesfully completed a koji build." in item.get(
                    "description"):
                logger.info(f"User: {account_login} is a packager in Fedora!")
                return True
        logger.info(
            f"Cannot verify whether user: {account_login} is a packager in Fedora."
        )
        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[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, if automatic verification of user
        (check if user is packager in fedora) fails, account is still inserted in whitelist
         with status : `waiting`.
         Then a scripts in files/scripts have to be executed for manual approval
        :param github_app: github app installation info
        :return: was the account auto-whitelisted?
        """
        account = self.get_account(github_app.account_login)
        if account:
            # the account is already in DB
            return True
        # we want to verify if user who installed the application is packager
        if Whitelist._is_packager(github_app.sender_login):
            github_app.status = WhitelistStatus.approved_automatically
            self.db[github_app.account_login] = github_app.get_dict()
            logger.info(f"Account {github_app.account_login} whitelisted!")
            return True
        else:
            logger.error(
                "Failed to verify that user is Fedora packager. "
                "This could be caused by different github username than FAS username "
                "or that user is not a packager.")
            github_app.status = WhitelistStatus.waiting
            self.db[github_app.account_login] = github_app.get_dict()
            logger.info(f"Account {github_app.account_login} inserted "
                        f"to whitelist with status: waiting for approval")
            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"User: {account_name} removed from whitelist!")
            return True
        else:
            logger.info(f"User: {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) -> bool:
        """
        Check if account is approved and report status back in case of PR
        :param event: PullRequest and Release TODO: handle more
        :param project: GitProject
        :return:
        """
        account_name = None
        if isinstance(event, PullRequestEvent):
            account_name = event.base_repo_namespace
        if isinstance(event, ReleaseEvent):
            account_name = event.repo_namespace

        if account_name:
            if not self.is_approved(account_name):
                logger.error(
                    f"User {account_name} is not approved on whitelist!")
                # TODO also check blacklist,
                # but for that we need to know who triggered the action
                if event.trigger == JobTriggerType.pull_request:
                    r = BuildStatusReporter(project, event.commit_sha, None)
                    msg = "Account is not whitelisted!"
                    r.report(
                        "failure",
                        msg,
                        url=FAQ_URL,
                        check_name=PRCheckName.get_build_check(),
                    )
                return False

        return True