Blob Blame Raw
# -*- coding: utf-8 -*-

"""
 (c) 2015-2018 - Copyright Red Hat Inc

 Authors:
   Pierre-Yves Chibon <pingou@pingoured.fr>

"""

from __future__ import unicode_literals

import flask
import logging

from sqlalchemy.exc import SQLAlchemyError
from six import string_types
from pygit2 import GitError, Repository

import pagure
import pagure.forms
import pagure.exceptions
import pagure.lib.git
import pagure.lib.query
import pagure.utils
from pagure.api import (
    API,
    api_method,
    APIERROR,
    api_login_required,
    get_authorized_api_project,
    api_login_optional,
    get_request_data,
    get_page,
    get_per_page,
)
from pagure.config import config as pagure_config


_log = logging.getLogger(__name__)


@API.route("/<repo>/git/tags")
@API.route("/<namespace>/<repo>/git/tags")
@API.route("/fork/<username>/<repo>/git/tags")
@API.route("/fork/<username>/<namespace>/<repo>/git/tags")
@api_method
def api_git_tags(repo, username=None, namespace=None):
    """
    Project git tags
    ----------------
    List the tags made on the project Git repository.

    ::

        GET /api/0/<repo>/git/tags
        GET /api/0/<namespace>/<repo>/git/tags

    ::

        GET /api/0/fork/<username>/<repo>/git/tags
        GET /api/0/fork/<username>/<namespace>/<repo>/git/tags

    Parameters
    ^^^^^^^^^^

    +-----------------+----------+---------------+--------------------------+
    | Key             | Type     | Optionality   | Description              |
    +=================+==========+===============+==========================+
    | ``with_commits``| string   | Optional      | | Include the commit hash|
    |                 |          |               |   corresponding to the   |
    |                 |          |               |   tags found in the repo |
    +-----------------+----------+---------------+--------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "total_tags": 2,
          "tags": ["0.0.1", "0.0.2"]
        }


        {
          "total_tags": 2,
          "tags": {
            "0.0.1": "bb8fa2aa199da08d6085e1c9badc3d83d188d38c",
            "0.0.2": "d16fe107eca31a1bdd66fb32c6a5c568e45b627e"
          }
        }

    """
    with_commits = pagure.utils.is_true(
        flask.request.values.get("with_commits", False)
    )

    repo = get_authorized_api_project(
        flask.g.session, repo, user=username, namespace=namespace
    )
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    tags = pagure.lib.git.get_git_tags(repo, with_commits=with_commits)

    jsonout = flask.jsonify({"total_tags": len(tags), "tags": tags})
    return jsonout


@API.route("/<repo>/watchers")
@API.route("/<namespace>/<repo>/watchers")
@API.route("/fork/<username>/<repo>/watchers")
@API.route("/fork/<username>/<namespace>/<repo>/watchers")
@api_method
def api_project_watchers(repo, username=None, namespace=None):
    """
    Project watchers
    ----------------
    List the watchers on the project.

    ::

        GET /api/0/<repo>/watchers
        GET /api/0/<namespace>/<repo>/watchers

    ::

        GET /api/0/fork/<username>/<repo>/watchers
        GET /api/0/fork/<username>/<namespace>/<repo>/watchers

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
            "total_watchers": 1,
            "watchers": {
                "mprahl": [
                    "issues",
                    "commits"
                ]
            }
        }
    """
    repo = get_authorized_api_project(
        flask.g.session, repo, user=username, namespace=namespace
    )
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    implicit_watch_users = {repo.user.username}
    for access_type in repo.access_users.keys():
        implicit_watch_users = implicit_watch_users | set(
            [user.username for user in repo.access_users[access_type]]
        )

    watching_users_to_watch_level = {}
    for implicit_watch_user in implicit_watch_users:
        user_watch_level = pagure.lib.query.get_watch_level_on_repo(
            flask.g.session, implicit_watch_user, repo
        )
        watching_users_to_watch_level[implicit_watch_user] = user_watch_level

    for access_type in repo.access_groups.keys():
        group_names = [
            "@" + group.group_name for group in repo.access_groups[access_type]
        ]
        for group_name in group_names:
            if group_name not in watching_users_to_watch_level:
                watching_users_to_watch_level[group_name] = set()
            # By the logic in pagure.lib.query.get_watch_level_on_repo, group
            # members only by default watch issues.  If they want to watch
            # commits they have to explicitly subscribe.
            watching_users_to_watch_level[group_name].add("issues")

    for key in watching_users_to_watch_level:
        watching_users_to_watch_level[key] = list(
            watching_users_to_watch_level[key]
        )

    # Get the explicit watch statuses
    for watcher in repo.watchers:
        if watcher.watch_issues or watcher.watch_commits:
            watching_users_to_watch_level[
                watcher.user.username
            ] = pagure.lib.query.get_watch_level_on_repo(
                flask.g.session, watcher.user.username, repo
            )
        else:
            if watcher.user.username in watching_users_to_watch_level:
                watching_users_to_watch_level.pop(watcher.user.username, None)

    return flask.jsonify(
        {
            "total_watchers": len(watching_users_to_watch_level),
            "watchers": watching_users_to_watch_level,
        }
    )


@API.route("/<repo>/git/urls")
@API.route("/<namespace>/<repo>/git/urls")
@API.route("/fork/<username>/<repo>/git/urls")
@API.route("/fork/<username>/<namespace>/<repo>/git/urls")
@api_login_optional()
@api_method
def api_project_git_urls(repo, username=None, namespace=None):
    """
    Project Git URLs
    ----------------
    List the Git URLS on the project.

    ::

        GET /api/0/<repo>/git/urls
        GET /api/0/<namespace>/<repo>/git/urls

    ::

        GET /api/0/fork/<username>/<repo>/git/urls
        GET /api/0/fork/<username>/<namespace>/<repo>/git/urls

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
            "total_urls": 2,
            "urls": {
                "ssh": "ssh://git@pagure.io/mprahl-test123.git",
                "git": "https://pagure.io/mprahl-test123.git"
            }
        }
    """
    repo = get_authorized_api_project(
        flask.g.session, repo, user=username, namespace=namespace
    )
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
    git_urls = {}

    git_url_ssh = pagure_config.get("GIT_URL_SSH")
    if pagure.utils.api_authenticated() and git_url_ssh:
        try:
            git_url_ssh = git_url_ssh.format(
                username=flask.g.fas_user.username
            )
        except (KeyError, IndexError):
            pass

    if git_url_ssh:
        git_urls["ssh"] = "{0}{1}.git".format(git_url_ssh, repo.fullname)
    if pagure_config.get("GIT_URL_GIT"):
        git_urls["git"] = "{0}{1}.git".format(
            pagure_config["GIT_URL_GIT"], repo.fullname
        )

    return flask.jsonify({"total_urls": len(git_urls), "urls": git_urls})


@API.route("/<repo>/git/branches")
@API.route("/<namespace>/<repo>/git/branches")
@API.route("/fork/<username>/<repo>/git/branches")
@API.route("/fork/<username>/<namespace>/<repo>/git/branches")
@api_method
def api_git_branches(repo, username=None, namespace=None):
    """
    List project branches
    ---------------------
    List the branches associated with a Pagure git repository

    ::

        GET /api/0/<repo>/git/branches
        GET /api/0/<namespace>/<repo>/git/branches

    ::

        GET /api/0/fork/<username>/<repo>/git/branches
        GET /api/0/fork/<username>/<namespace>/<repo>/git/branches

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "total_branches": 2,
          "branches": ["master", "dev"]
        }

    """
    repo = get_authorized_api_project(
        flask.g.session, repo, user=username, namespace=namespace
    )
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    branches = pagure.lib.git.get_git_branches(repo)

    return flask.jsonify(
        {"total_branches": len(branches), "branches": branches}
    )


@API.route("/projects")
@api_method
def api_projects():
    """
    List projects
    --------------
    Search projects given the specified criterias.

    ::

        GET /api/0/projects

    ::

        GET /api/0/projects?tags=fedora-infra

    ::

        GET /api/0/projects?page=1&per_page=50

    Parameters
    ^^^^^^^^^^

    +---------------+----------+---------------+--------------------------+
    | Key           | Type     | Optionality   | Description              |
    +===============+==========+===============+==========================+
    | ``tags``      | string   | Optional      | | Filters the projects   |
    |               |          |               |   returned by their tags |
    +---------------+----------+---------------+--------------------------+
    | ``pattern``   | string   | Optional      | | Filters the projects   |
    |               |          |               |   by the pattern string  |
    +---------------+----------+---------------+--------------------------+
    | ``username``  | string   | Optional      | | Filters the projects   |
    |               |          |               |   returned by the users  |
    |               |          |               |   having commit rights   |
    |               |          |               |   to it                  |
    +---------------+----------+---------------+--------------------------+
    | ``owner``     | string   | Optional      | | Filters the projects   |
    |               |          |               |   by ownership           |
    +---------------+----------+---------------+--------------------------+
    | ``namespace`` | string   | Optional      | | Filters the projects   |
    |               |          |               |   by namespace           |
    +---------------+----------+---------------+--------------------------+
    | ``fork``      | boolean  | Optional      | | Filters the projects   |
    |               |          |               |   returned depending if  |
    |               |          |               |   they are forks or not  |
    +---------------+----------+---------------+--------------------------+
    | ``short``     | boolean  | Optional      | | Whether to return the  |
    |               |          |               |   entrie project JSON    |
    |               |          |               |   or just a sub-set      |
    +---------------+----------+---------------+--------------------------+
    | ``page``      | int      | Optional      | | Specifies which        |
    |               |          |               |   page to return         |
    |               |          |               |   (defaults to: 1)       |
    +---------------+----------+---------------+--------------------------+
    | ``per_page``  | int      | Optional      | | The number of projects |
    |               |          |               |   to return per page.    |
    |               |          |               |   The maximum is 100.    |
    +---------------+----------+---------------+--------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "args": {
            "fork": null,
            "namespace": null,
            "owner": null,
            "page": 1,
            "pattern": null,
            "per_page": 2,
            "short": false,
            "tags": [],
            "username": null
          },
          "pagination": {
            "first": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=1",
            "last": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=500",
            "next": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=2",
            "page": 1,
            "pages": 500,
            "per_page": 2,
            "prev": null
          },
          "projects": [
            {
              "access_groups": {
                "admin": [],
                "commit": [],
                "ticket": []
              },
              "access_users": {
                "admin": [],
                "commit": [],
                "owner": [
                  "mprahl"
                ],
                "ticket": []
              },
              "close_status": [],
              "custom_keys": [],
              "date_created": "1498841289",
              "description": "test1",
              "fullname": "test1",
              "id": 1,
              "milestones": {},
              "name": "test1",
              "namespace": null,
              "parent": null,
              "priorities": {},
              "tags": [],
              "url_path": "test1",
              "user": {
                "fullname": "Matt Prahl",
                "name": "mprahl"
              }
            },
            {
              "access_groups": {
                "admin": [],
                "commit": [],
                "ticket": []
              },
              "access_users": {
                "admin": [],
                "commit": [],
                "owner": [
                  "mprahl"
                ],
                "ticket": []
              },
              "close_status": [],
              "custom_keys": [],
              "date_created": "1499795310",
              "description": "test2",
              "fullname": "test2",
              "id": 2,
              "milestones": {},
              "name": "test2",
              "namespace": null,
              "parent": null,
              "priorities": {},
              "tags": [],
              "url_path": "test2",
              "user": {
                "fullname": "Matt Prahl",
                "name": "mprahl"
              }
            }
          ],
          "total_projects": 1000
        }
    """
    tags = flask.request.values.getlist("tags")
    username = flask.request.values.get("username", None)
    fork = flask.request.values.get("fork", None)
    namespace = flask.request.values.get("namespace", None)
    owner = flask.request.values.get("owner", None)
    pattern = flask.request.values.get("pattern", None)
    short = pagure.utils.is_true(flask.request.values.get("short", False))

    if fork is not None:
        fork = pagure.utils.is_true(fork)

    private = False
    if pagure.utils.authenticated() and username == flask.g.fas_user.username:
        private = flask.g.fas_user.username

    project_count = pagure.lib.query.search_projects(
        flask.g.session,
        username=username,
        fork=fork,
        tags=tags,
        pattern=pattern,
        private=private,
        namespace=namespace,
        owner=owner,
        count=True,
    )

    # Pagination code inspired by Flask-SQLAlchemy
    page = get_page()
    per_page = get_per_page()
    pagination_metadata = pagure.lib.query.get_pagination_metadata(
        flask.request, page, per_page, project_count
    )
    query_start = (page - 1) * per_page
    query_limit = per_page

    projects = pagure.lib.query.search_projects(
        flask.g.session,
        username=username,
        fork=fork,
        tags=tags,
        pattern=pattern,
        private=private,
        namespace=namespace,
        owner=owner,
        limit=query_limit,
        start=query_start,
    )

    # prepare the output json
    jsonout = {
        "total_projects": project_count,
        "projects": projects,
        "args": {
            "tags": tags,
            "username": username,
            "fork": fork,
            "pattern": pattern,
            "namespace": namespace,
            "owner": owner,
            "short": short,
        },
    }

    if not short:
        projects = [p.to_json(api=True, public=True) for p in projects]
    else:
        projects = [
            {
                "name": p.name,
                "namespace": p.namespace,
                "fullname": p.fullname.replace("forks/", "fork/", 1)
                if p.fullname.startswith("forks/")
                else p.fullname,
                "description": p.description,
            }
            for p in projects
        ]

    jsonout["projects"] = projects
    if pagination_metadata:
        jsonout["args"]["page"] = page
        jsonout["args"]["per_page"] = per_page
        jsonout["pagination"] = pagination_metadata
    return flask.jsonify(jsonout)


@API.route("/<repo>")
@API.route("/<namespace>/<repo>")
@API.route("/fork/<username>/<repo>")
@API.route("/fork/<username>/<namespace>/<repo>")
@api_method
def api_project(repo, username=None, namespace=None):
    """
    Project information
    -------------------
    Return information about a specific project

    ::

        GET /api/0/<repo>
        GET /api/0/<namespace>/<repo>

    ::

        GET /api/0/fork/<username>/<repo>
        GET /api/0/fork/<username>/<namespace>/<repo>

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "access_groups": {
            "admin": [],
            "commit": [],
            "ticket": []
          },
          "access_users": {
            "admin": [
              "ryanlerch"
            ],
            "commit": [
              "puiterwijk"
            ],
            "owner": [
              "pingou"
            ],
            "ticket": [
              "vivekanand1101",
              "mprahl",
              "jcline",
              "lslebodn",
              "cverna",
              "farhaan"
            ]
          },
          "close_status": [
            "Invalid",
            "Insufficient data",
            "Fixed",
            "Duplicate"
          ],
          "custom_keys": [],
          "date_created": "1431549490",
          "date_modified": "1431549490",
          "description": "A git centered forge",
          "fullname": "pagure",
          "id": 10,
          "milestones": {},
          "name": "pagure",
          "namespace": null,
          "parent": null,
          "priorities": {},
          "tags": [
            "pagure",
            "fedmsg"
          ],
          "user": {
            "fullname": "Pierre-YvesChibon",
            "name": "pingou"
          }
        }

    """
    repo = get_authorized_api_project(
        flask.g.session, repo, user=username, namespace=namespace
    )

    expand_group = pagure.utils.is_true(
        flask.request.values.get("expand_group", False)
    )

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    output = repo.to_json(api=True, public=True)

    if expand_group:
        group_details = {}
        for grp in repo.projects_groups:
            group_details[grp.group.group_name] = [
                user.username for user in grp.group.users
            ]
        output["group_details"] = group_details

    jsonout = flask.jsonify(output)
    return jsonout


@API.route("/new/", methods=["POST"])
@API.route("/new", methods=["POST"])
@api_login_required(acls=["create_project"])
@api_method
def api_new_project():
    """
    Create a new project
    --------------------
    Create a new project on this pagure instance.

    This is an asynchronous call.

    ::

        POST /api/0/new


    Input
    ^^^^^

    +----------------------------+---------+--------------+---------------------------+
    | Key                        | Type    | Optionality  | Description               |
    +============================+=========+==============+===========================+
    | ``name``                   | string  | Mandatory    | | The name of the new     |
    |                            |         |              |   project.                |
    +----------------------------+---------+--------------+---------------------------+
    | ``description``            | string  | Mandatory    | | A short description of  |
    |                            |         |              |   the new project.        |
    +----------------------------+---------+--------------+---------------------------+
    | ``namespace``              | string  | Optional     | | The namespace of the    |
    |                            |         |              |   project to fork.        |
    +----------------------------+---------+--------------+---------------------------+
    | ``url``                    | string  | Optional     | | An url providing more   |
    |                            |         |              |   information about the   |
    |                            |         |              |   project.                |
    +----------------------------+---------+--------------+---------------------------+
    | ``avatar_email``           | string  | Optional     | | An email address for the|
    |                            |         |              |   avatar of the project.  |
    +----------------------------+---------+--------------+---------------------------+
    | ``create_readme``          | boolean | Optional     | | A boolean to specify if |
    |                            |         |              |   there should be a readme|
    |                            |         |              |   added to the project on |
    |                            |         |              |   creation.               |
    +----------------------------+---------+--------------+---------------------------+
    | ``private``                | boolean | Optional     | | A boolean to specify if |
    |                            |         |              |   the project to create   |
    |                            |         |              |   is private.             |
    |                            |         |              |   Note: not all pagure    |
    |                            |         |              |   instance support private|
    |                            |         |              |   projects, confirm this  |
    |                            |         |              |   with your administrators|
    +----------------------------+---------+--------------+---------------------------+
    | ``ignore_existing_repos``  | boolean | Optional     | | Only available to admins|
    |                            |         |              |   this option allows them |
    |                            |         |              |   to make project creation|
    |                            |         |              |   pass even if there is   |
    |                            |         |              |   already a coresopnding  |
    |                            |         |              |   git repository on disk  |
    +----------------------------+---------+--------------+---------------------------+
    | ``repospanner_region``     | boolean | Optional     | | Only available to admins|
    |                            |         |              |   this option allows them |
    |                            |         |              |   to override the default |
    |                            |         |              |   respoSpanner region     |
    |                            |         |              |   configured              |
    +----------------------------+---------+--------------+---------------------------+
    | ``wait``                   | boolean | Optional     | | A boolean to specify if |
    |                            |         |              |   this API call should    |
    |                            |         |              |   return a taskid or if it|
    |                            |         |              |   should wait for the task|
    |                            |         |              |   to finish.              |
    +----------------------------+---------+--------------+---------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        wait=False:
        {
          'message': 'Project creation queued',
          'taskid': '123-abcd'
        }

        wait=True:
        {
          'message': 'Project creation queued'
        }

    """  # noqa
    user = pagure.lib.query.search_user(
        flask.g.session, username=flask.g.fas_user.username
    )
    output = {}

    if not pagure_config.get("ENABLE_NEW_PROJECTS", True):
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ENEWPROJECTDISABLED
        )

    namespaces = pagure_config["ALLOWED_PREFIX"][:]
    if user:
        namespaces.extend([grp for grp in user.groups])

    form = pagure.forms.ProjectForm(namespaces=namespaces, csrf_enabled=False)
    if form.validate_on_submit():
        name = form.name.data
        description = form.description.data
        namespace = form.namespace.data
        url = form.url.data
        avatar_email = form.avatar_email.data
        create_readme = form.create_readme.data

        if namespace:
            namespace = namespace.strip()

        private = False
        if pagure_config.get("PRIVATE_PROJECTS", False):
            private = form.private.data
        if form.repospanner_region:
            repospanner_region = form.repospanner_region.data
        else:
            repospanner_region = None
        if form.ignore_existing_repos:
            ignore_existing_repos = form.ignore_existing_repos.data
        else:
            ignore_existing_repos = False

        try:
            task = pagure.lib.query.new_project(
                flask.g.session,
                name=name,
                namespace=namespace,
                repospanner_region=repospanner_region,
                ignore_existing_repo=ignore_existing_repos,
                description=description,
                private=private,
                url=url,
                avatar_email=avatar_email,
                user=flask.g.fas_user.username,
                blacklist=pagure_config["BLACKLISTED_PROJECTS"],
                allowed_prefix=pagure_config["ALLOWED_PREFIX"],
                add_readme=create_readme,
                userobj=user,
                prevent_40_chars=pagure_config.get(
                    "OLD_VIEW_COMMIT_ENABLED", False
                ),
                user_ns=pagure_config.get("USER_NAMESPACE", False),
            )
            flask.g.session.commit()
            output = {"message": "Project creation queued", "taskid": task.id}

            if get_request_data().get("wait", True):
                result = task.get()
                project = pagure.lib.query._get_project(
                    flask.g.session,
                    name=result["repo"],
                    namespace=result["namespace"],
                )
                output = {"message": 'Project "%s" created' % project.fullname}
        except pagure.exceptions.PagureException as err:
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.ENOCODE, error=str(err)
            )
        except SQLAlchemyError as err:  # pragma: no cover
            _log.exception(err)
            flask.g.session.rollback()
            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
    else:
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
        )

    jsonout = flask.jsonify(output)
    return jsonout


@API.route("/<repo>", methods=["PATCH"])
@API.route("/<namespace>/<repo>", methods=["PATCH"])
@api_login_required(acls=["modify_project"])
@api_method
def api_modify_project(repo, namespace=None):
    """
    Modify a project
    ----------------
    Modify an existing project on this Pagure instance.

    ::

        PATCH /api/0/<repo>


    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``main_admin``   | string  | Mandatory    | | The new main admin of   |
    |                  |         |              |   the project.            |
    +------------------+---------+--------------+---------------------------+
    | ``retain_access``| string  | Optional     | | The old main admin      |
    |                  |         |              |   retains access on the   |
    |                  |         |              |   project when giving the |
    |                  |         |              |   project. Defaults to    |
    |                  |         |              |   ``False``.              |
    +------------------+---------+--------------+---------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "access_groups": {
            "admin": [],
            "commit": [],
            "ticket": []
          },
          "access_users": {
            "admin": [],
            "commit": [],
            "owner": [
              "testuser1"
            ],
            "ticket": []
          },
          "close_status": [],
          "custom_keys": [],
          "date_created": "1496326387",
          "description": "Test",
          "fullname": "test-project2",
          "id": 2,
          "milestones": {},
          "name": "test-project2",
          "namespace": null,
          "parent": null,
          "priorities": {},
          "tags": [],
          "user": {
            "default_email": "testuser1@domain.local",
            "emails": [],
            "fullname": "Test User1",
            "name": "testuser1"
          }
        }

    """
    project = get_authorized_api_project(
        flask.g.session, repo, namespace=namespace
    )
    if not project:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if flask.g.token.project and project != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    is_site_admin = pagure.utils.is_admin()
    admins = [u.username for u in project.get_project_users("admin")]
    # Only allow the main admin, the admins of the project, and Pagure site
    # admins to modify projects, even if the user has the right ACLs on their
    # token
    if (
        flask.g.fas_user.username not in admins
        and flask.g.fas_user.username != project.user.username
        and not is_site_admin
    ):
        raise pagure.exceptions.APIError(
            401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
        )

    valid_keys = ["main_admin", "retain_access"]
    # Check if it's JSON or form data
    if flask.request.headers.get("Content-Type") == "application/json":
        # Set force to True to ignore the mimetype. Set silent so that None is
        # returned if it's invalid JSON.
        args = flask.request.get_json(force=True, silent=True) or {}
        retain_access = args.get("retain_access", False)
    else:
        args = get_request_data()
        retain_access = args.get("retain_access", "").lower() in ["true", "1"]

    if not args:
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    # Check to make sure there aren't parameters we don't support
    for key in args.keys():
        if key not in valid_keys:
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.EINVALIDREQ
            )

    if "main_admin" in args:
        if (
            flask.g.fas_user.username != project.user.username
            and not is_site_admin
        ):
            raise pagure.exceptions.APIError(
                401, error_code=APIERROR.ENOTMAINADMIN
            )
        # If the main_admin is already set correctly, don't do anything
        if flask.g.fas_user.username == project.user:
            return flask.jsonify(project.to_json(public=False, api=True))

        try:
            new_main_admin = pagure.lib.query.get_user(
                flask.g.session, args["main_admin"]
            )
        except pagure.exceptions.PagureException:
            raise pagure.exceptions.APIError(400, error_code=APIERROR.ENOUSER)

        old_main_admin = project.user.user
        pagure.lib.query.set_project_owner(
            flask.g.session, project, new_main_admin
        )
        if retain_access and flask.g.fas_user.username == old_main_admin:
            pagure.lib.query.add_user_to_project(
                flask.g.session,
                project,
                new_user=flask.g.fas_user.username,
                user=flask.g.fas_user.username,
            )

    try:
        flask.g.session.commit()
    except SQLAlchemyError:  # pragma: no cover
        flask.g.session.rollback()
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

    pagure.lib.git.generate_gitolite_acls(project=project)

    return flask.jsonify(project.to_json(public=False, api=True))


@API.route("/fork/", methods=["POST"])
@API.route("/fork", methods=["POST"])
@api_login_required(acls=["fork_project"])
@api_method
def api_fork_project():
    """
    Fork a project
    --------------------
    Fork a project on this pagure instance.

    This is an asynchronous call.

    ::

        POST /api/0/fork


    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``repo``         | string  | Mandatory    | | The name of the project |
    |                  |         |              |   to fork.                |
    +------------------+---------+--------------+---------------------------+
    | ``namespace``    | string  | Optional     | | The namespace of the    |
    |                  |         |              |   project to fork.        |
    +------------------+---------+--------------+---------------------------+
    | ``username``     | string  | Optional     | | The username of the user|
    |                  |         |              |   of the fork.            |
    +------------------+---------+--------------+---------------------------+
    | ``wait``         | boolean | Optional     | | A boolean to specify if |
    |                  |         |              |   this API call should    |
    |                  |         |              |   return a taskid or if it|
    |                  |         |              |   should wait for the task|
    |                  |         |              |   to finish.              |
    +------------------+---------+--------------+---------------------------+


    Sample response
    ^^^^^^^^^^^^^^^

    ::

        wait=False:
        {
          "message": "Project forking queued",
          "taskid": "123-abcd"
        }

        wait=True:
        {
          "message": 'Repo "test" cloned to "pingou/test"
        }

    """
    output = {}

    form = pagure.forms.ForkRepoForm(csrf_enabled=False)
    if form.validate_on_submit():
        repo = form.repo.data
        username = form.username.data or None
        namespace = form.namespace.data.strip() or None

        repo = get_authorized_api_project(
            flask.g.session, repo, user=username, namespace=namespace
        )
        if repo is None:
            raise pagure.exceptions.APIError(
                404, error_code=APIERROR.ENOPROJECT
            )

        try:
            task = pagure.lib.query.fork_project(
                flask.g.session, user=flask.g.fas_user.username, repo=repo
            )
            flask.g.session.commit()
            output = {"message": "Project forking queued", "taskid": task.id}

            if get_request_data().get("wait", True):
                task.get()
                output = {
                    "message": 'Repo "%s" cloned to "%s/%s"'
                    % (repo.fullname, flask.g.fas_user.username, repo.fullname)
                }
        except pagure.exceptions.PagureException as err:
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.ENOCODE, error=str(err)
            )
        except SQLAlchemyError as err:  # pragma: no cover
            _log.exception(err)
            flask.g.session.rollback()
            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
    else:
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
        )

    jsonout = flask.jsonify(output)
    return jsonout


@API.route("/<repo>/git/generateacls", methods=["POST"])
@API.route("/<namespace>/<repo>/git/generateacls", methods=["POST"])
@API.route("/fork/<username>/<repo>/git/generateacls", methods=["POST"])
@API.route(
    "/fork/<username>/<namespace>/<repo>/git/generateacls", methods=["POST"]
)
@api_login_required(acls=["generate_acls_project"])
@api_method
def api_generate_acls(repo, username=None, namespace=None):
    """
    Generate Gitolite ACLs on a project
    -----------------------------------
    Generate Gitolite ACLs on a project. This is restricted to Pagure admins.

    This is an asynchronous call.

    ::

        POST /api/0/rpms/python-requests/git/generateacls


    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``wait``         | boolean | Optional     | | A boolean to specify if |
    |                  |         |              |   this API call should    |
    |                  |         |              |   return a taskid or if it|
    |                  |         |              |   should wait for the task|
    |                  |         |              |   to finish.              |
    +------------------+---------+--------------+---------------------------+


    Sample response
    ^^^^^^^^^^^^^^^

    ::

        wait=False:
        {
          'message': 'Project ACL generation queued',
          'taskid': '123-abcd'
        }

        wait=True:
        {
          'message': 'Project ACLs generated'
        }

    """
    project = get_authorized_api_project(
        flask.g.session, repo, namespace=namespace
    )
    if not project:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if flask.g.token.project and project != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    # Check if it's JSON or form data
    if flask.request.headers.get("Content-Type") == "application/json":
        # Set force to True to ignore the mimetype. Set silent so that None is
        # returned if it's invalid JSON.
        json = flask.request.get_json(force=True, silent=True) or {}
        wait = json.get("wait", False)
    else:
        wait = pagure.utils.is_true(get_request_data().get("wait"))

    try:
        task = pagure.lib.git.generate_gitolite_acls(project=project)

        if wait:
            task.get()
            output = {"message": "Project ACLs generated"}
        else:
            output = {
                "message": "Project ACL generation queued",
                "taskid": task.id,
            }
    except pagure.exceptions.PagureException as err:
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.ENOCODE, error=str(err)
        )

    jsonout = flask.jsonify(output)
    return jsonout


@API.route("/<repo>/git/branch", methods=["POST"])
@API.route("/<namespace>/<repo>/git/branch", methods=["POST"])
@API.route("/fork/<username>/<repo>/git/branch", methods=["POST"])
@API.route("/fork/<username>/<namespace>/<repo>/git/branch", methods=["POST"])
@api_login_required(acls=["create_branch"])
@api_method
def api_new_branch(repo, username=None, namespace=None):
    """
    Create a new git branch on a project
    ------------------------------------
    Create a new git branch on a project

    ::

        POST /api/0/rpms/python-requests/git/branch


    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``branch``       | string  | Mandatory    | | A string of the branch  |
    |                  |         |              |   to create.              |
    +------------------+---------+--------------+---------------------------+
    | ``from_branch``  | string  | Optional     | | A string of the branch  |
    |                  |         |              |   to branch off of. This  |
    |                  |         |              |   defaults to "master".   |
    |                  |         |              |   if ``from_commit``      |
    |                  |         |              |   isn't set.              |
    +------------------+---------+--------------+---------------------------+
    | ``from_commit``  | string  | Optional     | | A string of the commit  |
    |                  |         |              |   to branch off of.       |
    +------------------+---------+--------------+---------------------------+


    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          'message': 'Project branch was created'
        }

    """
    project = get_authorized_api_project(
        flask.g.session, repo, namespace=namespace
    )
    if not project:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if flask.g.token.project and project != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    # Check if it's JSON or form data
    if flask.request.headers.get("Content-Type") == "application/json":
        # Set force to True to ignore the mimetype. Set silent so that None is
        # returned if it's invalid JSON.
        args = flask.request.get_json(force=True, silent=True) or {}
    else:
        args = get_request_data()

    branch = args.get("branch")
    from_branch = args.get("from_branch")
    from_commit = args.get("from_commit")

    if from_branch and from_commit:
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    if (
        not branch
        or not isinstance(branch, string_types)
        or (from_branch and not isinstance(from_branch, string_types))
        or (from_commit and not isinstance(from_commit, string_types))
    ):
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    try:
        pagure.lib.git.new_git_branch(
            flask.g.fas_user.username,
            project,
            branch,
            from_branch=from_branch,
            from_commit=from_commit,
        )
    except GitError:  # pragma: no cover
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EGITERROR)
    except pagure.exceptions.PagureException as error:
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.ENOCODE, error=str(error)
        )

    output = {"message": "Project branch was created"}
    jsonout = flask.jsonify(output)
    return jsonout


@API.route("/<repo>/c/<commit_hash>/flag")
@API.route("/<namespace>/<repo>/c/<commit_hash>/flag")
@API.route("/fork/<username>/<repo>/c/<commit_hash>/flag")
@API.route("/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag")
@api_method
def api_commit_flags(repo, commit_hash, username=None, namespace=None):
    """
    Flags for a commit
    ------------------
    Return all flags for given commit of given project

    ::

        GET /api/0/<repo>/c/<commit_hash>/flag
        GET /api/0/<namespace>/<repo>/c/<commit_hash>/flag

    ::

        GET /api/0/fork/<username>/<repo>/c/<commit_hash>/flag
        GET /api/0/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "flags": [
            {
              "comment": "flag-comment",
              "commit_hash": "28f1f7fe844301f0e5f7aecacae0a1e5ec50a090",
              "date_created": "1520341983",
              "percent": null,
              "status": "success",
              "url": "https://some.url.com",
              "user": {
                "fullname": "Full name",
                "name": "fname"
              },
              "username": "somename"
            },
            {
              "comment": "different-comment",
              "commit_hash": "28f1f7fe844301f0e5f7aecacae0a1e5ec50a090",
              "date_created": "1520512543",
              "percent": null,
              "status": "pending",
              "url": "https://other.url.com",
              "user": {
                "fullname": "Other Name",
                "name": "oname"
              },
              "username": "differentname"
            }
          ],
          "total_flags": 2
        }

    """
    repo = get_authorized_api_project(
        flask.g.session, repo, user=username, namespace=namespace
    )
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    reponame = pagure.utils.get_repo_path(repo)
    repo_obj = Repository(reponame)
    try:
        repo_obj.get(commit_hash)
    except ValueError:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOCOMMIT)

    flags = pagure.lib.query.get_commit_flag(
        flask.g.session, repo, commit_hash
    )
    flags = [f.to_json(public=True) for f in flags]
    return flask.jsonify({"total_flags": len(flags), "flags": flags})


@API.route("/<repo>/c/<commit_hash>/flag", methods=["POST"])
@API.route("/<namespace>/<repo>/c/<commit_hash>/flag", methods=["POST"])
@API.route("/fork/<username>/<repo>/c/<commit_hash>/flag", methods=["POST"])
@API.route(
    "/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag",
    methods=["POST"],
)
@api_login_required(acls=["commit_flag"])
@api_method
def api_commit_add_flag(repo, commit_hash, username=None, namespace=None):
    """
    Flag a commit
    -------------------
    Add or edit flags on a commit.

    ::

        POST /api/0/<repo>/c/<commit_hash>/flag
        POST /api/0/<namespace>/<repo>/c/<commit_hash>/flag

    ::

        POST /api/0/fork/<username>/<repo>/c/<commit_hash>/flag
        POST /api/0/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag

    Input
    ^^^^^

    +---------------+---------+--------------+-----------------------------+
    | Key           | Type    | Optionality  | Description                 |
    +===============+=========+==============+=============================+
    | ``username``  | string  | Mandatory    | | The name of the           |
    |               |         |              |   application to be         |
    |               |         |              |   presented to users        |
    |               |         |              |   on the commit pages       |
    +---------------+---------+--------------+-----------------------------+
    | ``comment``   | string  | Mandatory    | | A short message           |
    |               |         |              |   summarizing the           |
    |               |         |              |   presented results         |
    +---------------+---------+--------------+-----------------------------+
    | ``url``       | string  | Mandatory    | | A URL to the result       |
    |               |         |              |   of this flag              |
    +---------------+---------+--------------+-----------------------------+
    | ``status``    | string  | Mandatory    | | The status of the task,   |
    |               |         |              |   can be any of:            |
    |               |         |              |   $$FLAG_STATUSES_COMMAS$$  |
    +---------------+---------+--------------+-----------------------------+
    | ``percent``   | int     | Optional     | | A percentage of           |
    |               |         |              |   completion compared to    |
    |               |         |              |   the goal. The percentage  |
    |               |         |              |   also determine the        |
    |               |         |              |   background color of the   |
    |               |         |              |   flag on the pages         |
    +---------------+---------+--------------+-----------------------------+
    | ``uid``       | string  | Optional     | | A unique identifier used  |
    |               |         |              |   to identify a flag across |
    |               |         |              |   all projects. If the      |
    |               |         |              |   provided UID matches an   |
    |               |         |              |   existing one, then the    |
    |               |         |              |   API call will update the  |
    |               |         |              |   existing one rather than  |
    |               |         |              |   create a new one.         |
    |               |         |              |   Maximum Length: 32        |
    |               |         |              |   characters. Default: an   |
    |               |         |              |   auto generated UID        |
    +---------------+---------+--------------+-----------------------------+


    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "flag": {
              "comment": "Tests passed",
              "commit_hash": "62b49f00d489452994de5010565fab81",
              "date_created": "1510742565",
              "percent": 100,
              "status": "success",
              "url": "http://jenkins.cloud.fedoraproject.org/",
              "user": {
                "default_email": "bar@pingou.com",
                "emails": ["bar@pingou.com", "foo@pingou.com"],
                "fullname": "PY C",
                "name": "pingou"},
              "username": "Jenkins"
            },
            "message": "Flag added",
            "uid": "b1de8f80defd4a81afe2e09f39678087"
        }

    ::

        {
          "flag": {
              "comment": "Tests passed",
              "commit_hash": "62b49f00d489452994de5010565fab81",
              "date_created": "1510742565",
              "percent": 100,
              "status": "success",
              "url": "http://jenkins.cloud.fedoraproject.org/",
              "user": {
                "default_email": "bar@pingou.com",
                "emails": ["bar@pingou.com", "foo@pingou.com"],
                "fullname": "PY C",
                "name": "pingou"},
              "username": "Jenkins"
            },
            "message": "Flag updated",
            "uid": "b1de8f80defd4a81afe2e09f39678087"
        }

    """  # noqa

    repo = get_authorized_api_project(
        flask.g.session, repo, user=username, namespace=namespace
    )

    output = {}

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if flask.g.token.project and repo != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    reponame = pagure.utils.get_repo_path(repo)
    repo_obj = Repository(reponame)
    try:
        repo_obj.get(commit_hash)
    except ValueError:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOCOMMIT)

    form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False)
    if form.validate_on_submit():
        username = form.username.data
        percent = form.percent.data.strip() or None
        comment = form.comment.data.strip()
        url = form.url.data.strip()
        uid = form.uid.data.strip() if form.uid.data else None
        status = form.status.data.strip()
        try:
            # New Flag
            message, uid = pagure.lib.query.add_commit_flag(
                session=flask.g.session,
                repo=repo,
                commit_hash=commit_hash,
                username=username,
                percent=percent,
                comment=comment,
                status=status,
                url=url,
                uid=uid,
                user=flask.g.fas_user.username,
                token=flask.g.token.id,
            )
            flask.g.session.commit()
            c_flag = pagure.lib.query.get_commit_flag_by_uid(
                flask.g.session, commit_hash, uid
            )
            output["message"] = message
            output["uid"] = uid
            output["flag"] = c_flag.to_json()
        except pagure.exceptions.PagureException as err:
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.ENOCODE, error=str(err)
            )
        except SQLAlchemyError as err:  # pragma: no cover
            flask.g.session.rollback()
            _log.exception(err)
            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
    else:
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
        )

    jsonout = flask.jsonify(output)
    return jsonout


@API.route("/<repo>/watchers/update", methods=["POST"])
@API.route("/<namespace>/<repo>/watchers/update", methods=["POST"])
@API.route("/fork/<username>/<repo>/watchers/update", methods=["POST"])
@API.route(
    "/fork/<username>/<namespace>/<repo>/watchers/update", methods=["POST"]
)
@api_login_required(acls=["update_watch_status"])
@api_method
def api_update_project_watchers(repo, username=None, namespace=None):
    """
    Update project watchers
    -----------------------
    Allows anyone to update their own watch status on the project.

    ::

        POST /api/0/<repo>/watchers/update
        POST /api/0/<namespace>/<repo>/watchers/update

    ::

        POST /api/0/fork/<username>/<repo>/watchers/update
        POST /api/0/fork/<username>/<namespace>/<repo>/watchers/update

    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``repo``         | string  | Mandatory    | | The name of the project |
    |                  |         |              |   to fork.                |
    +------------------+---------+--------------+---------------------------+
    | ``status``       | string  | Mandatory    | | The new watch status to |
    |                  |         |              |   set on that project.    |
    |                  |         |              |   (See options below)     |
    +------------------+---------+--------------+---------------------------+
    | ``watcher``      | string  | Mandatory    | | The name of the user    |
    |                  |         |              |   changing their watch    |
    |                  |         |              |   status.                 |
    +------------------+---------+--------------+---------------------------+
    | ``namespace``    | string  | Optional     | | The namespace of the    |
    |                  |         |              |   project to fork.        |
    +------------------+---------+--------------+---------------------------+
    | ``username``     | string  | Optional     | | The username of the user|
    |                  |         |              |   of the fork.            |
    +------------------+---------+--------------+---------------------------+

    Watch Status
    ^^^^^^^^^^^^

    +------------+----------------------------------------------+
    | Key        | Description                                  |
    +============+==============================================+
    | -1         | Reset the watch status to default            |
    +------------+----------------------------------------------+
    | 0          | Unwatch, don't notify the user of anything   |
    +------------+----------------------------------------------+
    | 1          | Watch issues and pull-requests               |
    +------------+----------------------------------------------+
    | 2          | Watch commits                                |
    +------------+----------------------------------------------+
    | 3          | Watch commits, issues and pull-requests      |
    +------------+----------------------------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
            "message": "You are now watching issues and PRs on this project",
            "status": "ok"
        }
    """

    project = get_authorized_api_project(
        flask.g.session, repo, namespace=namespace
    )
    if not project:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if flask.g.token.project and project != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    # Get the input submitted
    data = get_request_data()

    watcher = data.get("watcher")

    if not watcher:
        _log.debug("api_update_project_watchers: Invalid watcher: %s", watcher)
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    is_site_admin = pagure.utils.is_admin()
    # Only allow the main admin, and the user themselves to update their
    # status
    if not is_site_admin and flask.g.fas_user.username != watcher:
        raise pagure.exceptions.APIError(
            401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
        )

    try:
        pagure.lib.query.get_user(flask.g.session, watcher)
    except pagure.exceptions.PagureException:
        _log.debug(
            "api_update_project_watchers: Invalid user watching: %s", watcher
        )
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    watch_status = data.get("status")

    try:
        msg = pagure.lib.query.update_watch_status(
            session=flask.g.session,
            project=project,
            user=watcher,
            watch=watch_status,
        )
        flask.g.session.commit()
    except pagure.exceptions.PagureException as err:
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.ENOCODE, error=str(err)
        )
    except SQLAlchemyError as err:  # pragma: no cover
        flask.g.session.rollback()
        _log.exception(err)
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

    return flask.jsonify({"message": msg, "status": "ok"})


@API.route("/<repo>/git/modifyacls", methods=["POST"])
@API.route("/<namespace>/<repo>/git/modifyacls", methods=["POST"])
@API.route("/fork/<username>/<repo>/git/modifyacls", methods=["POST"])
@API.route(
    "/fork/<username>/<namespace>/<repo>/git/modifyacls", methods=["POST"]
)
@api_login_required(acls=["modify_project"])
@api_method
def api_modify_acls(repo, namespace=None, username=None):
    """
    Modify ACLs on a project
    ------------------------
    Add, remove or update ACLs on a project for a particular user or group.

    This is restricted to project admins.

    ::

        POST /api/0/<repo>/modifyacls/flag
        POST /api/0/<namespace>/<repo>/modifyacls/flag

    ::

        POST /api/0/fork/<username>/<repo>/modifyacls/flag
        POST /api/0/fork/<username>/<namespace>/<repo>/modifyacls/flag


    Input
    ^^^^^

    +------------------+---------+---------------+---------------------------+
    | Key              | Type    | Optionality   | Description               |
    +==================+=========+===============+===========================+
    | ``user_type``    | String  | Mandatory     | A string to specify if    |
    |                  |         |               | the ACL should be changed |
    |                  |         |               | for a user or a group.    |
    |                  |         |               | Specifying one of either  |
    |                  |         |               | 'user' or 'group' is      |
    |                  |         |               | mandatory                 |
    |                  |         |               |                           |
    +------------------+---------+---------------+---------------------------+
    | ``name``         | String  | Mandatory     | The name of the user or   |
    |                  |         |               | group whose ACL           |
    |                  |         |               | should be changed.        |
    |                  |         |               |                           |
    +------------------+---------+---------------+---------------------------+
    | ``acl``          | String  | Optional      | Can be either unspecified,|
    |                  |         |               | 'ticket', 'commit',       |
    |                  |         |               | 'admin'. If unspecified,  |
    |                  |         |               | the access will be removed|
    |                  |         |               |                           |
    +------------------+---------+---------------+---------------------------+


    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "access_groups": {
            "admin": [],
            "commit": [],
            "ticket": []
          },
          "access_users": {
            "admin": [],
            "commit": [
              "ta2"
            ],
            "owner": [
              "karsten"
            ],
            "ticket": [
              "ta1"
            ]
          },
          "close_status": [],
          "custom_keys": [],
          "date_created": "1531131619",
          "date_modified": "1531302337",
          "description": "pagure local instance",
          "fullname": "pagure",
          "id": 1,
          "milestones": {},
          "name": "pagure",
          "namespace": null,
          "parent": null,
          "priorities": {},
          "tags": [],
          "url_path": "pagure",
          "user": {
            "fullname": "KH",
            "name": "karsten"
          }
        }

    """
    output = {}
    project = get_authorized_api_project(
        flask.g.session, repo, namespace=namespace
    )
    if not project:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if flask.g.token.project and project != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    form = pagure.forms.ModifyACLForm(csrf_enabled=False)
    if form.validate_on_submit():
        acl = form.acl.data
        group = None
        user = None
        if form.user_type.data == "user":
            user = form.name.data
        else:
            group = form.name.data

        is_site_admin = pagure.utils.is_admin()
        admins = [u.username for u in project.get_project_users("admin")]

        if not acl:
            if (
                user
                and flask.g.fas_user.username != user
                and flask.g.fas_user.username not in admins
                and flask.g.fas_user.username != project.user.username
                and not is_site_admin
            ):
                raise pagure.exceptions.APIError(
                    401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
                )
        elif (
            flask.g.fas_user.username not in admins
            and flask.g.fas_user.username != project.user.username
            and not is_site_admin
        ):
            raise pagure.exceptions.APIError(
                401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
            )

        if user:
            user_obj = pagure.lib.query.search_user(
                flask.g.session, username=user
            )
            if not user_obj:
                raise pagure.exceptions.APIError(
                    404, error_code=APIERROR.ENOUSER
                )

        elif group:
            group_obj = pagure.lib.query.search_groups(
                flask.g.session, group_name=group
            )
            if not group_obj:
                raise pagure.exceptions.APIError(
                    404, error_code=APIERROR.ENOGROUP
                )

        if acl:
            if (
                user
                and user_obj not in project.access_users[acl]
                and user_obj.user != project.user.user
            ):
                _log.info(
                    "Adding user %s to project: %s", user, project.fullname
                )
                pagure.lib.query.add_user_to_project(
                    session=flask.g.session,
                    project=project,
                    new_user=user,
                    user=flask.g.fas_user.username,
                    access=acl,
                )
            elif group and group_obj not in project.access_groups[acl]:
                _log.info(
                    "Adding group %s to project: %s", group, project.fullname
                )
                pagure.lib.query.add_group_to_project(
                    session=flask.g.session,
                    project=project,
                    new_group=group,
                    user=flask.g.fas_user.username,
                    access=acl,
                    create=pagure_config.get("ENABLE_GROUP_MNGT", False),
                    is_admin=pagure.utils.is_admin(),
                )
        else:
            if user:
                _log.info(
                    "Looking at removing user %s from project %s",
                    user,
                    project.fullname,
                )
                try:
                    pagure.lib.query.remove_user_of_project(
                        flask.g.session,
                        user_obj,
                        project,
                        flask.g.fas_user.username,
                    )
                except pagure.exceptions.PagureException as err:
                    raise pagure.exceptions.APIError(
                        400, error_code=APIERROR.EINVALIDREQ, errors="%s" % err
                    )
            elif group:
                pass

        try:
            flask.g.session.commit()
        except pagure.exceptions.PagureException as msg:
            flask.g.session.rollback()
            _log.debug(msg)
            flask.flash(str(msg), "error")
        except SQLAlchemyError as err:
            _log.exception(err)
            flask.g.session.rollback()
            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

        pagure.lib.git.generate_gitolite_acls(project=project)
        output = project.to_json(api=True, public=True)
    else:
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
        )

    jsonout = flask.jsonify(output)
    return jsonout