# -*- coding: utf-8 -*-
"""
(c) 2015-2017 - Copyright Red Hat Inc
Authors:
Pierre-Yves Chibon <pingou@pingoured.fr>
Internal endpoints.
"""
from __future__ import unicode_literals, absolute_import
import collections
import logging
import os
import flask
import pygit2
import werkzeug
from functools import wraps
from sqlalchemy.exc import SQLAlchemyError
PV = flask.Blueprint("internal_ns", __name__, url_prefix="/pv")
import pagure # noqa: E402
import pagure.exceptions # noqa: E402
import pagure.forms # noqa: E402
import pagure.lib.git # noqa: E402
import pagure.lib.query # noqa: E402
import pagure.lib.tasks # noqa: E402
import pagure.utils # noqa: E402
import pagure.ui.fork # noqa: E402
from pagure.config import config as pagure_config # noqa: E402
_log = logging.getLogger(__name__)
MERGE_OPTIONS = {
"NO_CHANGE": {
"short_code": "No changes",
"message": "Nothing to change, git is up to date",
},
"FFORWARD": {
"short_code": "Ok",
"message": "The pull-request can be merged and fast-forwarded",
},
"CONFLICTS": {
"short_code": "Conflicts",
"message": "The pull-request cannot be merged due to conflicts",
},
"MERGE": {
"short_code": "With merge",
"message": "The pull-request can be merged with a merge commit",
},
}
def internal_access_only(function):
""" Decorator used to check if the request is iternal or not.
The request must either come from one of the addresses listed
in IP_ALLOWED_INTERNAL or it must have the "Authentication"
header set to "token <admin_token>" and the token must
have "internal_access" ACL.
"""
@wraps(function)
def decorated_function(*args, **kwargs):
""" Wrapped function actually checking if the request is local.
"""
ip_allowed = pagure_config.get(
"IP_ALLOWED_INTERNAL", ["127.0.0.1", "localhost", "::1"]
)
if "Authorization" in flask.request.headers:
res = pagure.utils.check_api_acls(acls=["internal_access"])
if res:
return res
elif flask.request.remote_addr not in ip_allowed:
_log.debug(
"IP: %s is not in the list of allowed IPs: %s "
"and 'Authorization' header not provided"
% (flask.request.remote_addr, ip_allowed)
)
flask.abort(403)
return function(*args, **kwargs)
return decorated_function
@PV.route("/ssh/lookupkey/", methods=["POST"])
@internal_access_only
def lookup_ssh_key():
""" Looks up an SSH key by search_key for keyhelper.py """
search_key = flask.request.form["search_key"]
username = flask.request.form.get("username")
key = pagure.lib.query.find_ssh_key(flask.g.session, search_key, username)
if not key:
return flask.jsonify({"found": False})
result = {"found": True, "public_key": key.public_ssh_key}
if key.user:
result["username"] = key.user.username
elif key.project:
result["username"] = "deploykey_%s_%s" % (
werkzeug.secure_filename(key.project.fullname),
key.id,
)
else:
return flask.jsonify({"found": False})
return flask.jsonify(result)
@PV.route("/ssh/checkaccess/", methods=["POST"])
@internal_access_only
def check_ssh_access():
""" Determines whether a user has any access to the requested repo. """
gitdir = flask.request.form["gitdir"]
remoteuser = flask.request.form["username"]
# Build a fake path so we can use get_repo_info_from_path
path = os.path.join(pagure_config["GIT_FOLDER"], gitdir)
(
repotype,
project_user,
namespace,
repo,
) = pagure.lib.git.get_repo_info_from_path(path, hide_notfound=True)
if repo is None:
return flask.jsonify({"access": False})
project = pagure.lib.query.get_authorized_project(
flask.g.session,
repo,
user=project_user,
namespace=namespace,
asuser=remoteuser,
)
if not project:
return flask.jsonify({"access": False})
if repotype != "main" and not pagure.utils.is_repo_user(
project, remoteuser
):
return flask.jsonify({"access": False})
return flask.jsonify(
{
"access": True,
"reponame": gitdir,
"repospanner_reponame": project._repospanner_repo_name(repotype)
if project.is_on_repospanner
else None,
"repopath": path,
"repotype": repotype,
"region": project.repospanner_region,
"project_name": project.name,
"project_user": project.user.username if project.is_fork else None,
"project_namespace": project.namespace,
}
)
@PV.route("/pull-request/comment/", methods=["PUT"])
@internal_access_only
def pull_request_add_comment():
""" Add a comment to a pull-request.
"""
pform = pagure.forms.ProjectCommentForm(csrf_enabled=False)
if not pform.validate_on_submit():
flask.abort(400, description="Invalid request")
objid = pform.objid.data
useremail = pform.useremail.data
request = pagure.lib.query.get_request_by_uid(
flask.g.session, request_uid=objid
)
if not request:
flask.abort(404, description="Pull-request not found")
form = pagure.forms.AddPullRequestCommentForm(csrf_enabled=False)
if not form.validate_on_submit():
flask.abort(400, description="Invalid request")
commit = form.commit.data or None
tree_id = form.tree_id.data or None
filename = form.filename.data or None
row = form.row.data or None
comment = form.comment.data
try:
message = pagure.lib.query.add_pull_request_comment(
flask.g.session,
request=request,
commit=commit,
tree_id=tree_id,
filename=filename,
row=row,
comment=comment,
user=useremail,
)
flask.g.session.commit()
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.exception(err)
flask.abort(
500, description="Error when saving the request to the database"
)
return flask.jsonify({"message": message})
@PV.route("/ticket/comment/", methods=["PUT"])
@internal_access_only
def ticket_add_comment():
""" Add a comment to an issue.
"""
pform = pagure.forms.ProjectCommentForm(csrf_enabled=False)
if not pform.validate_on_submit():
flask.abort(400, description="Invalid request")
objid = pform.objid.data
useremail = pform.useremail.data
issue = pagure.lib.query.get_issue_by_uid(flask.g.session, issue_uid=objid)
if issue is None:
flask.abort(404, description="Issue not found")
user_obj = pagure.lib.query.search_user(flask.g.session, email=useremail)
admin = False
if user_obj:
admin = user_obj.user == issue.project.user.user or (
user_obj.user in [user.user for user in issue.project.committers]
)
if (
issue.private
and user_obj
and not admin
and not issue.user.user == user_obj.username
):
flask.abort(
403,
description="This issue is private and you are not allowed "
"to view it",
)
form = pagure.forms.CommentForm(csrf_enabled=False)
if not form.validate_on_submit():
flask.abort(400, description="Invalid request")
comment = form.comment.data
try:
message = pagure.lib.query.add_issue_comment(
flask.g.session,
issue=issue,
comment=comment,
user=useremail,
notify=True,
)
flask.g.session.commit()
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.exception(err)
flask.abort(
500, description="Error when saving the request to the database"
)
return flask.jsonify({"message": message})
@PV.route("/pull-request/merge", methods=["POST"])
def mergeable_request_pull():
""" Returns if the specified pull-request can be merged or not.
"""
force = flask.request.form.get("force", False)
if force is not False:
force = True
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "CONFLICTS", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
requestid = flask.request.form.get("requestid")
request = pagure.lib.query.get_request_by_uid(
flask.g.session, request_uid=requestid
)
if not request:
response = flask.jsonify(
{"code": "CONFLICTS", "message": "Pull-request not found"}
)
response.status_code = 404
return response
merge_status = request.merge_status
if not merge_status or force:
username = None
if flask.g.authenticated:
username = flask.g.fas_user.username
try:
merge_status = pagure.lib.git.merge_pull_request(
session=flask.g.session,
request=request,
username=username,
domerge=False,
)
except pygit2.GitError as err:
response = flask.jsonify(
{"code": "CONFLICTS", "message": "%s" % err}
)
response.status_code = 409
return response
except pagure.exceptions.PagureException as err:
response = flask.jsonify(
{"code": "CONFLICTS", "message": "%s" % err}
)
response.status_code = 500
return response
threshold = request.project.settings.get(
"Minimum_score_to_merge_pull-request", -1
)
if threshold > 0 and int(request.score) < int(threshold):
response = flask.jsonify(
{
"code": "CONFLICTS",
"message": "Pull-Request does not meet the minimal "
"number of review required: %s/%s"
% (request.score, threshold),
}
)
response.status_code = 400
return response
return flask.jsonify(pagure.utils.get_merge_options(request, merge_status))
@PV.route("/pull-request/ready", methods=["POST"])
def get_pull_request_ready_branch():
""" Return the list of branches that have commits not in the main
branch/repo (thus for which one could open a PR) and the number of
commits that differ.
"""
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "ERROR", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
args_reponame = flask.request.form.get("repo", "").strip() or None
args_namespace = flask.request.form.get("namespace", "").strip() or None
args_user = flask.request.form.get("repouser", "").strip() or None
repo = pagure.lib.query.get_authorized_project(
flask.g.session,
args_reponame,
namespace=args_namespace,
user=args_user,
)
if not repo:
response = flask.jsonify(
{
"code": "ERROR",
"message": "No repo found with the information provided",
}
)
response.status_code = 404
return response
if repo.is_fork and repo.parent:
if not repo.parent.settings.get("pull_requests", True):
response = flask.jsonify(
{
"code": "ERROR",
"message": "Pull-request have been disabled for this repo",
}
)
response.status_code = 400
return response
else:
if not repo.settings.get("pull_requests", True):
response = flask.jsonify(
{
"code": "ERROR",
"message": "Pull-request have been disabled for this repo",
}
)
response.status_code = 400
return response
task = pagure.lib.tasks.pull_request_ready_branch.delay(
namespace=args_namespace, name=args_reponame, user=args_user
)
return flask.jsonify({"code": "OK", "task": task.id})
@PV.route("/<repo>/issue/template", methods=["POST"])
@PV.route("/<namespace>/<repo>/issue/template", methods=["POST"])
@PV.route("/fork/<username>/<repo>/issue/template", methods=["POST"])
@PV.route(
"/fork/<username>/<namespace>/<repo>/issue/template", methods=["POST"]
)
def get_ticket_template(repo, namespace=None, username=None):
""" Return the template asked for the specified project
"""
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "ERROR", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
template = flask.request.args.get("template", None)
if not template:
response = flask.jsonify(
{"code": "ERROR", "message": "No template provided"}
)
response.status_code = 400
return response
repo = pagure.lib.query.get_authorized_project(
flask.g.session, repo, user=username, namespace=namespace
)
if not repo.settings.get("issue_tracker", True):
response = flask.jsonify(
{
"code": "ERROR",
"message": "No issue tracker found for this project",
}
)
response.status_code = 404
return response
ticketrepopath = repo.repopath("tickets")
content = None
if os.path.exists(ticketrepopath):
ticketrepo = pygit2.Repository(ticketrepopath)
if not ticketrepo.is_empty and not ticketrepo.head_is_unborn:
commit = ticketrepo[ticketrepo.head.target]
# Get the asked template
content_file = pagure.utils.__get_file_in_tree(
ticketrepo,
commit.tree,
["templates", "%s.md" % template],
bail_on_tree=True,
)
if content_file:
content, _ = pagure.doc_utils.convert_readme(
content_file.data, "md"
)
if content:
response = flask.jsonify({"code": "OK", "message": content})
else:
response = flask.jsonify(
{"code": "ERROR", "message": "No such template found"}
)
response.status_code = 404
return response
@PV.route("/branches/commit/", methods=["POST"])
def get_branches_of_commit():
""" Return the list of branches that have the specified commit in
"""
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "ERROR", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
commit_id = flask.request.form.get("commit_id", "").strip() or None
if not commit_id:
response = flask.jsonify(
{"code": "ERROR", "message": "No commit id submitted"}
)
response.status_code = 400
return response
repo = pagure.lib.query.get_authorized_project(
flask.g.session,
flask.request.form.get("repo", "").strip() or None,
user=flask.request.form.get("repouser", "").strip() or None,
namespace=flask.request.form.get("namespace", "").strip() or None,
)
if not repo:
response = flask.jsonify(
{
"code": "ERROR",
"message": "No repo found with the information provided",
}
)
response.status_code = 404
return response
repopath = repo.repopath("main")
if not os.path.exists(repopath):
response = flask.jsonify(
{
"code": "ERROR",
"message": "No git repo found with the information provided",
}
)
response.status_code = 404
return response
repo_obj = pygit2.Repository(repopath)
try:
commit_id in repo_obj
except ValueError:
response = flask.jsonify(
{
"code": "ERROR",
"message": "This commit could not be found in this repo",
}
)
response.status_code = 404
return response
branches = []
if not repo_obj.head_is_unborn:
compare_branch = repo_obj.lookup_branch(repo_obj.head.shorthand)
else:
compare_branch = None
for branchname in repo_obj.listall_branches():
branch = repo_obj.lookup_branch(branchname)
if not repo_obj.is_empty and len(repo_obj.listall_branches()) > 1:
merge_commit = None
if compare_branch:
merge_commit_obj = repo_obj.merge_base(
compare_branch.peel().hex, branch.peel().hex
)
if merge_commit_obj:
merge_commit = merge_commit_obj.hex
repo_commit = repo_obj[branch.peel().hex]
for commit in repo_obj.walk(
repo_commit.oid.hex, pygit2.GIT_SORT_NONE
):
if commit.oid.hex == merge_commit:
break
if commit.oid.hex == commit_id:
branches.append(branchname)
break
# If we didn't find the commit in any branch and there is one, then it
# is in the default branch.
if not branches and compare_branch:
branches.append(compare_branch.branch_name)
return flask.jsonify({"code": "OK", "branches": branches})
@PV.route("/branches/heads/", methods=["POST"])
def get_branches_head():
""" Return the heads of each branch in the repo, using the following
structure:
{
code: 'OK',
branches: {
name : commit,
...
},
heads: {
commit : [branch, ...],
...
}
}
"""
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "ERROR", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
repo = pagure.lib.query.get_authorized_project(
flask.g.session,
flask.request.form.get("repo", "").strip() or None,
namespace=flask.request.form.get("namespace", "").strip() or None,
user=flask.request.form.get("repouser", "").strip() or None,
)
if not repo:
response = flask.jsonify(
{
"code": "ERROR",
"message": "No repo found with the information provided",
}
)
response.status_code = 404
return response
repopath = repo.repopath("main")
if not os.path.exists(repopath):
response = flask.jsonify(
{
"code": "ERROR",
"message": "No git repo found with the information provided",
}
)
response.status_code = 404
return response
repo_obj = pygit2.Repository(repopath)
branches = {}
if not repo_obj.is_empty and len(repo_obj.listall_branches()) > 1:
for branchname in repo_obj.listall_branches():
branch = repo_obj.lookup_branch(branchname)
branches[branchname] = branch.peel().hex
# invert the dict
heads = collections.defaultdict(list)
for branch, commit in branches.items():
heads[commit].append(branch)
return flask.jsonify({"code": "OK", "branches": branches, "heads": heads})
@PV.route("/task/<taskid>", methods=["GET"])
def task_info(taskid):
""" Return the results of the specified task or a 418 if the task is
still being processed.
"""
task = pagure.lib.tasks.get_result(taskid)
if task.ready():
result = task.get(timeout=0, propagate=False)
if isinstance(result, Exception):
result = "%s" % result
return flask.jsonify({"results": result})
else:
flask.abort(418)
@PV.route("/stats/commits/authors", methods=["POST"])
def get_stats_commits():
""" Return statistics about the commits made on the specified repo.
"""
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "ERROR", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
repo = pagure.lib.query.get_authorized_project(
flask.g.session,
flask.request.form.get("repo", "").strip() or None,
namespace=flask.request.form.get("namespace", "").strip() or None,
user=flask.request.form.get("repouser", "").strip() or None,
)
if not repo:
response = flask.jsonify(
{
"code": "ERROR",
"message": "No repo found with the information provided",
}
)
response.status_code = 404
return response
repopath = repo.repopath("main")
task = pagure.lib.tasks.commits_author_stats.delay(repopath)
return flask.jsonify(
{
"code": "OK",
"message": "Stats asked",
"url": flask.url_for("internal_ns.task_info", taskid=task.id),
"task_id": task.id,
}
)
@PV.route("/stats/commits/trend", methods=["POST"])
def get_stats_commits_trend():
""" Return evolution of the commits made on the specified repo.
"""
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "ERROR", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
repo = pagure.lib.query.get_authorized_project(
flask.g.session,
flask.request.form.get("repo", "").strip() or None,
namespace=flask.request.form.get("namespace", "").strip() or None,
user=flask.request.form.get("repouser", "").strip() or None,
)
if not repo:
response = flask.jsonify(
{
"code": "ERROR",
"message": "No repo found with the information provided",
}
)
response.status_code = 404
return response
repopath = repo.repopath("main")
task = pagure.lib.tasks.commits_history_stats.delay(repopath)
return flask.jsonify(
{
"code": "OK",
"message": "Stats asked",
"url": flask.url_for("internal_ns.task_info", taskid=task.id),
"task_id": task.id,
}
)
@PV.route("/<repo>/family", methods=["POST"])
@PV.route("/<namespace>/<repo>/family", methods=["POST"])
@PV.route("/fork/<username>/<repo>/family", methods=["POST"])
@PV.route("/fork/<username>/<namespace>/<repo>/family", methods=["POST"])
def get_project_family(repo, namespace=None, username=None):
""" Return the family of projects for the specified project
{
code: 'OK',
family: [
]
}
"""
allows_pr = flask.request.form.get("allows_pr", "").lower().strip() in [
"1",
"true",
]
allows_issues = flask.request.form.get(
"allows_issues", ""
).lower().strip() in ["1", "true"]
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
response = flask.jsonify(
{"code": "ERROR", "message": "Invalid input submitted"}
)
response.status_code = 400
return response
repo = pagure.lib.query.get_authorized_project(
flask.g.session, repo, user=username, namespace=namespace
)
if not repo:
response = flask.jsonify(
{
"code": "ERROR",
"message": "No repo found with the information provided",
}
)
response.status_code = 404
return response
if allows_pr:
family = [
p.url_path
for p in pagure.lib.query.get_project_family(flask.g.session, repo)
if p.settings.get("pull_requests", True)
]
elif allows_issues:
family = [
p.url_path
for p in pagure.lib.query.get_project_family(flask.g.session, repo)
if p.settings.get("issue_tracker", True)
]
else:
family = [
p.url_path
for p in pagure.lib.query.get_project_family(flask.g.session, repo)
]
return flask.jsonify({"code": "OK", "family": family})