# -*- coding: utf-8 -*-
"""
(c) 2015-2016 - Copyright Red Hat Inc
Authors:
Pierre-Yves Chibon <pingou@pingoured.fr>
API namespace version 0.
"""
# pylint: disable=invalid-name
# pylint: disable=too-few-public-methods
# pylint: disable=too-many-locals
from __future__ import unicode_literals, absolute_import
import codecs
import functools
import logging
import os
import docutils
import enum
import flask
import markupsafe
from six.moves.urllib_parse import urljoin
API = flask.Blueprint("api_ns", __name__, url_prefix="/api/0")
import pagure.lib.query # noqa: E402
import pagure.lib.tasks # noqa: E402
from pagure.config import config as pagure_config # noqa: E402
from pagure.doc_utils import load_doc, modify_rst, modify_html # noqa: E402
from pagure.exceptions import APIError # noqa: E402
from pagure.utils import authenticated, check_api_acls # noqa: E402
_log = logging.getLogger(__name__)
def preload_docs(endpoint):
""" Utility to load an RST file and turn it into fancy HTML. """
here = os.path.dirname(os.path.abspath(__file__))
fname = os.path.join(here, "..", "doc", endpoint + ".rst")
with codecs.open(fname, "r", "utf-8") as stream:
rst = stream.read()
rst = modify_rst(rst)
api_docs = docutils.examples.html_body(rst)
api_docs = modify_html(api_docs)
api_docs = markupsafe.Markup(api_docs)
return api_docs
APIDOC = preload_docs("api")
class APIERROR(enum.Enum):
""" Clast listing as Enum all the possible error thrown by the API.
"""
ENOCODE = "Variable message describing the issue"
ENOPROJECT = "Project not found"
ENOPROJECTS = "No projects found"
ETRACKERDISABLED = "Issue tracker disabled for this project"
EDBERROR = (
"An error occurred at the database level and prevent the "
+ "action from reaching completion"
)
EINVALIDREQ = "Invalid or incomplete input submitted"
EINVALIDTOK = (
"Invalid or expired token. Please visit %s to get or "
"renew your API token."
% urljoin(pagure_config["APP_URL"], "settings#api-keys")
)
ENOISSUE = "Issue not found"
EISSUENOTALLOWED = "You are not allowed to view this issue"
EPRNOTALLOWED = "You are not allowed to view this pull-request"
EPULLREQUESTSDISABLED = (
"Pull-Request have been deactivated for this project"
)
ENOREQ = "Pull-Request not found"
ENOPRCLOSE = (
"You are not allowed to merge/close pull-request for this project"
)
EPRSCORE = (
"This request does not have the minimum review score "
"necessary to be merged"
)
EPRCONFLICTS = "This pull-request conflicts and thus cannot be merged"
ENOTASSIGNEE = "Only the assignee can merge this review"
ENOTASSIGNED = "This request must be assigned to be merged"
ENOUSER = "No such user found"
ENOCOMMENT = "Comment not found"
ENEWPROJECTDISABLED = (
"Creating project have been disabled for this instance"
)
ETIMESTAMP = "Invalid timestamp format"
EDATETIME = "Invalid datetime format"
EINVALIDISSUEFIELD = "Invalid custom field submitted"
EINVALIDISSUEFIELD_LINK = (
"Invalid custom field submitted, the value is not a link"
)
EINVALIDPRIORITY = "Invalid priority submitted"
ENOGROUP = "Group not found"
ENOTMAINADMIN = "Only the main admin can set the main admin of a project"
EMODIFYPROJECTNOTALLOWED = "You are not allowed to modify this project"
EINVALIDPERPAGEVALUE = "The per_page value must be between 1 and 100"
EGITERROR = "An error occurred during a git operation"
ENOCOMMIT = "No such commit found in this repository"
ENOTHIGHENOUGH = (
"You do not have sufficient permissions to perform this action"
)
ENOSIGNEDOFF = (
"This repo enforces that all commits are signed off "
"by their author."
)
ETRACKERREADONLY = "The issue tracker of this project is read-only"
ENOPRSTATS = "No statistics could be computed for this PR"
EUBLOCKED = "You have been blocked from this project"
EREBASENOTALLOWED = "You are not authorized to rebase this pull-request"
def get_authorized_api_project(session, repo, user=None, namespace=None):
""" Helper function to get an authorized_project with optional lock. """
repo = pagure.lib.query.get_authorized_project(
flask.g.session, repo, user=user, namespace=namespace
)
flask.g.repo = repo
return repo
def get_request_data():
return flask.request.form or flask.request.get_json() or {}
def api_login_required(acls=None):
""" Decorator used to indicate that authentication is required for some
API endpoint.
"""
def decorator(function):
""" The decorator of the function """
@functools.wraps(function)
def decorated_function(*args, **kwargs):
""" Actually does the job with the arguments provided. """
response = check_api_acls(acls)
if response:
return response
# Block all POST request from blocked users
if flask.request.method == "POST":
# Retrieve the variables in the URL
url_args = flask.request.view_args or {}
# Check if there is a `repo` and an `username`
repo = url_args.get("repo")
username = url_args.get("username")
namespace = url_args.get("namespace")
if repo:
flask.g.repo = pagure.lib.query.get_authorized_project(
flask.g.session,
repo,
user=username,
namespace=namespace,
)
if (
flask.g.repo
and flask.g.fas_user.username
in flask.g.repo.block_users
):
output = {
"error": APIERROR.EUBLOCKED.value,
"error_code": APIERROR.EUBLOCKED.name,
}
response = flask.jsonify(output)
response.status_code = 403
return response
return function(*args, **kwargs)
return decorated_function
return decorator
def api_login_optional(acls=None):
""" Decorator used to indicate that authentication is optional for some
API endpoint.
"""
def decorator(function):
""" The decorator of the function """
@functools.wraps(function)
def decorated_function(*args, **kwargs):
""" Actually does the job with the arguments provided. """
response = check_api_acls(acls, optional=True)
if response:
return response
return function(*args, **kwargs)
return decorated_function
return decorator
def api_method(function):
""" Runs an API endpoint and catch all the APIException thrown. """
@functools.wraps(function)
def wrapper(*args, **kwargs):
""" Actually does the job with the arguments provided. """
try:
result = function(*args, **kwargs)
except APIError as err:
if err.error_code in [APIERROR.EDBERROR]:
_log.exception(err)
if err.error_code in [APIERROR.ENOCODE]:
output = {
"error": err.error,
"error_code": err.error_code.name,
}
else:
output = {
"error": err.error_code.value,
"error_code": err.error_code.name,
}
if err.errors:
output["errors"] = err.errors
response = flask.jsonify(output)
response.status_code = err.status_code
else:
response = result
return response
return wrapper
def get_page():
""" Returns the page value specified in the request.
Defaults to 1.
raises APIERROR.EINVALIDREQ if the page provided isn't an integer
raises APIERROR.EINVALIDREQ if the page provided is lower than 1
"""
page = flask.request.values.get("page", None)
if not page:
page = 1
else:
try:
page = int(page)
except (TypeError, ValueError):
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ
)
if page < 1:
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ
)
return page
def get_per_page():
""" Returns the per_page value specified in the request.
Defaults to 20.
raises APIERROR.EINVALIDREQ if the page provided isn't an integer
raises APIERROR.EINVALIDPERPAGEVALUE if the page provided is lower
than 1 or greater than 100
"""
per_page = flask.request.values.get("per_page", None) or 20
if per_page:
try:
per_page = int(per_page)
except (TypeError, ValueError):
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ
)
if per_page < 1 or per_page > 100:
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDPERPAGEVALUE
)
return per_page
if pagure_config.get("ENABLE_TICKETS", True):
from pagure.api import issue # noqa: E402
from pagure.api import fork # noqa: E402
from pagure.api import project # noqa: E402
from pagure.api import user # noqa: E402
from pagure.api import group # noqa: E402
if pagure_config.get("PAGURE_CI_SERVICES", False):
from pagure.api.ci import jenkins # noqa: E402
@API.route("/version/")
@API.route("/version")
@API.route("/-/version")
def api_version():
"""
API Version
-----------
Get the current API version.
::
GET /api/0/-/version
Sample response
^^^^^^^^^^^^^^^
::
{
"version": "1"
}
"""
return flask.jsonify({"version": pagure.__api_version__})
@API.route("/users/")
@API.route("/users")
def api_users():
"""
List users
-----------
Retrieve users that have logged into the Pagure instance.
This can then be used as input for autocompletion in some forms/fields.
::
GET /api/0/users
Parameters
^^^^^^^^^^
+---------------+----------+---------------+------------------------------+
| Key | Type | Optionality | Description |
+===============+==========+===============+==============================+
| ``pattern`` | string | Optional | | Filters the starting |
| | | | letters of the usernames |
+---------------+----------+---------------+------------------------------+
Sample response
^^^^^^^^^^^^^^^
::
{
"total_users": 2,
"users": ["user1", "user2"]
}
"""
pattern = flask.request.args.get("pattern", None)
if pattern is not None and not pattern.endswith("*"):
pattern += "*"
users = pagure.lib.query.search_user(flask.g.session, pattern=pattern)
return flask.jsonify(
{
"total_users": len(users),
"users": [usr.username for usr in users],
"mention": [
{
"username": usr.username,
"name": usr.fullname,
"image": pagure.lib.query.avatar_url_from_email(
usr.default_email, size=16
),
}
for usr in users
],
}
)
@API.route("/-/whoami", methods=["POST"])
@api_login_optional()
def api_whoami():
"""
Who am I?
---------
This API endpoint will return the username associated with the provided
API token.
::
POST /api/0/-/whoami
Sample response
^^^^^^^^^^^^^^^
::
{
"username": "user1"
}
"""
if authenticated():
return flask.jsonify({"username": flask.g.fas_user.username})
else:
output = {
"error_code": APIERROR.EINVALIDTOK.name,
"error": APIERROR.EINVALIDTOK.value,
}
jsonout = flask.jsonify(output)
jsonout.status_code = 401
return jsonout
@API.route("/task/<taskid>/status")
@API.route("/task/<taskid>/status/")
def api_task_status(taskid):
"""
Return the status of a async task
"""
result = pagure.lib.tasks.get_result(taskid)
if not result.ready():
output = {"ready": False, "status": result.status}
else:
output = {
"ready": True,
"successful": result.successful(),
"status": result.status,
}
return flask.jsonify(output)
@API.route("/<repo>/tags")
@API.route("/<repo>/tags/")
@API.route("/fork/<username>/<repo>/tags")
@API.route("/fork/<username>/<repo>/tags/")
def api_project_tags(repo, username=None):
"""
List all the tags of a project
------------------------------
List the tags made on the project's issues.
::
GET /api/0/<repo>/tags
::
GET /api/0/fork/<username>/<repo>/tags
Parameters
^^^^^^^^^^
+---------------+----------+---------------+--------------------------+
| Key | Type | Optionality | Description |
+===============+==========+===============+==========================+
| ``pattern`` | string | Optional | | Filters the starting |
| | | | letters of the tags |
+---------------+----------+---------------+--------------------------+
Sample response
^^^^^^^^^^^^^^^
::
{
"total_tags": 2,
"tags": ["tag1", "tag2"]
}
"""
pattern = flask.request.args.get("pattern", None)
if pattern is not None and not pattern.endswith("*"):
pattern += "*"
project_obj = get_authorized_api_project(flask.g.session, repo, username)
if not project_obj:
output = {"output": "notok", "error": "Project not found"}
jsonout = flask.jsonify(output)
jsonout.status_code = 404
return jsonout
tags = pagure.lib.query.get_tags_of_project(
flask.g.session, project_obj, pattern=pattern
)
return flask.jsonify(
{"total_tags": len(tags), "tags": [tag.tag for tag in tags]}
)
@API.route("/error_codes/")
@API.route("/error_codes")
@API.route("/-/error_codes")
def api_error_codes():
"""
Error codes
------------
Get a dictionary (hash) of all error codes.
::
GET /api/0/-/error_codes
Sample response
^^^^^^^^^^^^^^^
::
{
ENOCODE: 'Variable message describing the issue',
ENOPROJECT: 'Project not found',
}
"""
errors = {
val.name: val.value for val in APIERROR.__members__.values()
} # pylint: disable=no-member
return flask.jsonify(errors)
@API.route("/")
def api():
""" Display the api information page. """
api_project_doc = load_doc(project.api_project)
api_projects_doc = load_doc(project.api_projects)
api_project_watchers_doc = load_doc(project.api_project_watchers)
api_git_tags_doc = load_doc(project.api_git_tags)
api_project_git_urls_doc = load_doc(project.api_project_git_urls)
api_git_branches_doc = load_doc(project.api_git_branches)
api_new_project_doc = load_doc(project.api_new_project)
api_modify_project_doc = load_doc(project.api_modify_project)
api_fork_project_doc = load_doc(project.api_fork_project)
api_modify_acls_doc = load_doc(project.api_modify_acls)
api_generate_acls_doc = load_doc(project.api_generate_acls)
api_new_branch_doc = load_doc(project.api_new_branch)
api_commit_flags_doc = load_doc(project.api_commit_flags)
api_commit_add_flag_doc = load_doc(project.api_commit_add_flag)
api_update_project_watchers_doc = load_doc(
project.api_update_project_watchers
)
api_get_project_options_doc = load_doc(project.api_get_project_options)
api_modify_project_options_doc = load_doc(
project.api_modify_project_options
)
api_project_block_user_doc = load_doc(project.api_project_block_user)
issues = []
if pagure_config.get("ENABLE_TICKETS", True):
issues.append(load_doc(issue.api_new_issue))
issues.append(load_doc(issue.api_view_issues))
issues.append(load_doc(issue.api_view_issue))
issues.append(load_doc(issue.api_view_issue_comment))
issues.append(load_doc(issue.api_comment_issue))
issues.append(load_doc(issue.api_update_custom_field))
issues.append(load_doc(issue.api_update_custom_fields))
issues.append(load_doc(issue.api_change_status_issue))
issues.append(load_doc(issue.api_change_milestone_issue))
issues.append(load_doc(issue.api_assign_issue))
issues.append(load_doc(issue.api_subscribe_issue))
issues.append(load_doc(user.api_view_user_issues))
ci_doc = []
if pagure_config.get("PAGURE_CI_SERVICES", False):
if "jenkins" in pagure_config["PAGURE_CI_SERVICES"]:
ci_doc.append(load_doc(jenkins.jenkins_ci_notification))
api_pull_request_create_doc = load_doc(fork.api_pull_request_create)
api_pull_request_views_doc = load_doc(fork.api_pull_request_views)
api_pull_request_view_doc = load_doc(fork.api_pull_request_view)
api_pull_request_diffstats_doc = load_doc(fork.api_pull_request_diffstats)
api_pull_request_by_uid_view_doc = load_doc(
fork.api_pull_request_by_uid_view
)
api_pull_request_merge_doc = load_doc(fork.api_pull_request_merge)
api_pull_request_rebase_doc = load_doc(fork.api_pull_request_rebase)
api_pull_request_close_doc = load_doc(fork.api_pull_request_close)
api_pull_request_add_comment_doc = load_doc(
fork.api_pull_request_add_comment
)
api_pull_request_add_flag_doc = load_doc(fork.api_pull_request_add_flag)
api_pull_request_assign_doc = load_doc(fork.api_pull_request_assign)
api_pull_request_update_doc = load_doc(fork.api_pull_request_update)
api_version_doc = load_doc(api_version)
api_whoami_doc = load_doc(api_whoami)
api_users_doc = load_doc(api_users)
api_view_user_doc = load_doc(user.api_view_user)
api_view_user_activity_stats_doc = load_doc(
user.api_view_user_activity_stats
)
api_view_user_activity_date_doc = load_doc(
user.api_view_user_activity_date
)
api_view_user_requests_filed_doc = load_doc(
user.api_view_user_requests_filed
)
api_view_user_requests_actionable_doc = load_doc(
user.api_view_user_requests_actionable
)
api_view_group_doc = load_doc(group.api_view_group)
api_groups_doc = load_doc(group.api_groups)
if pagure_config.get("ENABLE_TICKETS", True):
api_project_tags_doc = load_doc(api_project_tags)
api_error_codes_doc = load_doc(api_error_codes)
extras = [api_whoami_doc, api_version_doc, api_error_codes_doc]
if pagure_config.get("ENABLE_TICKETS", True):
extras.append(api_project_tags_doc)
return flask.render_template(
"api.html",
version=pagure.__api_version__,
api_doc=APIDOC,
projects=[
api_new_project_doc,
api_modify_project_doc,
api_project_doc,
api_projects_doc,
api_git_tags_doc,
api_project_git_urls_doc,
api_project_watchers_doc,
api_git_branches_doc,
api_fork_project_doc,
api_modify_acls_doc,
api_generate_acls_doc,
api_new_branch_doc,
api_commit_flags_doc,
api_commit_add_flag_doc,
api_update_project_watchers_doc,
api_get_project_options_doc,
api_modify_project_options_doc,
api_project_block_user_doc,
],
issues=issues,
requests=[
api_pull_request_create_doc,
api_pull_request_views_doc,
api_pull_request_view_doc,
api_pull_request_diffstats_doc,
api_pull_request_by_uid_view_doc,
api_pull_request_merge_doc,
api_pull_request_rebase_doc,
api_pull_request_close_doc,
api_pull_request_add_comment_doc,
api_pull_request_add_flag_doc,
api_pull_request_assign_doc,
api_pull_request_update_doc,
],
users=[
api_users_doc,
api_view_user_doc,
api_view_user_activity_stats_doc,
api_view_user_activity_date_doc,
api_view_user_requests_filed_doc,
api_view_user_requests_actionable_doc,
],
groups=[api_groups_doc, api_view_group_doc],
ci=ci_doc,
extras=extras,
)