예제 #1
0
def upgrade():
    bind = op.get_bind()
    session = orm.Session(bind=bind)

    db = Redis(
        host=getenv("REDIS_SERVICE_HOST", "localhost"),
        port=int(getenv("REDIS_SERVICE_PORT", "6379")),
        db=0,
        decode_responses=True,
    )

    # tasks
    keys = db.keys("celery-task-meta-*")
    for key in keys:
        data = loads(db.get(key))
        task_id = data.get("task_id")
        status = data.get("status")
        result = data.get("result")
        traceback = data.get("traceback")
        date_done = data.get("data_done")
        if isinstance(date_done, str):
            date_done = datetime.fromisoformat(date_done)

        logger.info(f"Adding task {task_id} into TaskResultModel")

        # our table
        TaskResultUpgradeModel.add_task_result(
            session=session, task_id=task_id, task_result_dict=result
        )
        # celery table
        add_task_to_celery_table(
            session=session,
            task_id=task_id,
            status=status,
            result=result,
            traceback=traceback,
            date_done=date_done,
        )

    # whitelist
    db = PersistentDict(hash_name="whitelist")
    for account, data in db.get_all().items():
        if not isinstance(data, dict):
            continue

        status = data.get("status")
        logger.info(f"Adding account {account} into WhitelistModel")
        WhitelistUpgradeModel.add_account(
            session=session, account_name=account, status=status
        )

    # installations
    for event in RedisInstallation.db().get_all().values():
        if not isinstance(event, dict):
            continue

        event = event["event_data"]
        account_login = event.get("account_login")
        account_id = event.get("account_id")
        account_url = event.get("account_url")
        account_type = event.get("account_type")
        sender_id = event.get("sender_id")
        sender_login = event.get("sender_login")

        created_at = event.get("created_at")
        if isinstance(created_at, (int, float)):
            created_at = datetime.fromtimestamp(created_at, timezone.utc)
        elif isinstance(created_at, str):
            created_at = created_at.replace("Z", "+00:00")
            created_at = datetime.fromisoformat(created_at)

        logger.info(f"Adding installation by {account_login} into InstallationModel")
        InstallationUpgradeModel.create(
            session=session,
            account_login=account_login,
            account_id=account_id,
            account_type=account_type,
            account_url=account_url,
            sender_login=sender_login,
            sender_id=sender_id,
            created_at=created_at,
        )

    #  copr-builds
    for copr_build in RedisCoprBuild.db().get_all().values():
        if not isinstance(copr_build, dict):
            continue

        project_name = copr_build.get("project")
        owner = copr_build.get("owner")
        chroots = copr_build.get("chroots")
        build_submitted_time = (
            datetime.fromisoformat(copr_build.get("build_submitted_time"))
            if copr_build.get("build_submitted_time")
            else datetime(2020, 1, 1, 0, 0, 0)
        )
        build_start_time = datetime(2020, 1, 1, 0, 10, 0)
        build_finished_time = datetime(2020, 1, 1, 0, 20, 0)
        build_id = copr_build.get("build_id")

        if not build_id:
            continue

        status = copr_build.get("status")
        web_url = (
            f"https://copr.fedorainfracloud.org/coprs/{owner}/{project_name}/"
            f"build/{build_id}/"
        )

        try:
            project_name_list = project_name.split("-")
            if project_name_list[-1] == "stg":
                pr_id = int(project_name_list[-2])
            else:
                pr_id = int(project_name_list[-1])

            job_trigger = JobTriggerUpgradeModel.get_or_create(
                type=JobTriggerModelType.pull_request,
                trigger_id=pr_id,
                session=session,
            )
        except Exception:
            continue

        logger.info(f"Adding copr build with build ID {build_id} into CoprBuildModel")
        for chroot in chroots:
            CoprBuildUpgradeModel.get_or_create(
                session=session,
                build_id=str(build_id),
                project_name=project_name,
                owner=owner,
                target=chroot,
                status=status,
                job_trigger=job_trigger,
                web_url=web_url,
                build_submitted_time=build_submitted_time,
                build_start_time=build_start_time,
                build_finished_time=build_finished_time,
            )

    session.commit()
예제 #2
0
 def db(cls) -> PersistentDict:
     if not cls.table_name:
         raise RuntimeError("table_name is not set")
     return PersistentDict(hash_name=cls.table_name)
예제 #3
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)
예제 #4
0
 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)
예제 #5
0
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from http import HTTPStatus
from logging import getLogger

from flask_restplus import Namespace, Resource
from persistentdict.dict_in_redis import PersistentDict

# hash name is defined in worker (Whitelist class), which I don't want to import from
from packit_service.service.events import Event

db = PersistentDict(hash_name="whitelist")

logger = getLogger("packit_service")

ns = Namespace("whitelist", description="Whitelisted FAS accounts")


@ns.route("")
class WhiteList(Resource):
    @ns.response(HTTPStatus.OK, "OK, whitelist follows")
    def get(self):
        """List all Whitelisted FAS accounts"""
        return [Event.ts2str(event) for event in db.get_all().values()]


@ns.route("/<string:login>")
예제 #6
0
 def __init__(self):
     self.db = PersistentDict(hash_name="blacklist")
예제 #7
0
 def task_data(self):
     """ get data which celery stores about a task """
     return PersistentDict(hash_name=self.identifier)
예제 #8
0
 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)
예제 #9
0
 def __init__(self):
     self.db = PersistentDict(hash_name="whitelist")
예제 #10
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
예제 #11
0
 def db(self):
     return PersistentDict(hash_name=str(uuid4()))