Return a JSON error instead of a 404 on certain malformed URLs.

This handles the case where a route is requested, but without a required
parameter.  So, /blueprints/info is requested instead of
/blueprints/info/http-server.  It accomplishes this via a decorator, so
a lot of these route-related functions now have quite a few decorators
attached to them.

Typo'd URLs (/blueprints/nfo for instance) will still return a 404.  I
think this is a reasonable thing to do.
This commit is contained in:
Chris Lumens 2018-08-03 15:46:19 -04:00
parent 8e948e4a4d
commit 5daf2d416a
2 changed files with 97 additions and 0 deletions

View File

@ -0,0 +1,44 @@
#
# Copyright (C) 2018 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
log = logging.getLogger("lorax-composer")
from flask import jsonify
from functools import update_wrapper
# A decorator for checking the parameters provided to the API route implementing
# functions. The tuples parameter is a list of tuples. Each tuple is the string
# name of a parameter ("blueprint_name", not blueprint_name), the value it's set
# to by flask if the caller did not provide it, and a message to be returned to
# the user.
#
# If the parameter is set to its default, the error message is returned. Otherwise,
# the decorated function is called and its return value is returned.
def checkparams(tuples):
def decorator(f):
def wrapped_function(*args, **kwargs):
for tup in tuples:
if kwargs[tup[0]] == tup[1]:
log.error("(%s) %s", f.__name__, tup[2])
return jsonify(status=False, errors=[tup[2]]), 400
return f(*args, **kwargs)
return update_wrapper(wrapped_function, f)
return decorator

View File

@ -980,6 +980,7 @@ from flask import jsonify, request, Response, send_file
import pytoml as toml
from pylorax.sysutils import joinpaths
from pylorax.api.checkparams import checkparams
from pylorax.api.compose import start_build, compose_types
from pylorax.api.crossdomain import crossdomain
from pylorax.api.projects import projects_list, projects_info, projects_depsolve
@ -1026,8 +1027,10 @@ def v0_api(api):
blueprints = take_limits(map(lambda f: f[:-5], list_branch_files(api.config["GITLOCK"].repo, branch)), offset, limit)
return jsonify(blueprints=blueprints, limit=limit, offset=offset, total=len(blueprints))
@api.route("/api/v0/blueprints/info", defaults={'blueprint_names': ""})
@api.route("/api/v0/blueprints/info/<blueprint_names>")
@crossdomain(origin="*")
@checkparams([("blueprint_names", "", "no blueprint names given")])
def v0_blueprints_info(blueprint_names):
"""Return the contents of the blueprint, or a list of blueprints"""
branch = request.args.get("branch", "master")
@ -1082,8 +1085,10 @@ def v0_api(api):
else:
return jsonify(changes=changes, blueprints=blueprints, errors=errors)
@api.route("/api/v0/blueprints/changes", defaults={'blueprint_names': ""})
@api.route("/api/v0/blueprints/changes/<blueprint_names>")
@crossdomain(origin="*")
@checkparams([("blueprint_names", "", "no blueprint names given")])
def v0_blueprints_changes(blueprint_names):
"""Return the changes to a blueprint or list of blueprints"""
branch = request.args.get("branch", "master")
@ -1134,8 +1139,10 @@ def v0_api(api):
else:
return jsonify(status=True)
@api.route("/api/v0/blueprints/delete", defaults={'blueprint_name': ""}, methods=["DELETE"])
@api.route("/api/v0/blueprints/delete/<blueprint_name>", methods=["DELETE"])
@crossdomain(origin="*")
@checkparams([("blueprint_name", "", "no blueprint name given")])
def v0_blueprints_delete(blueprint_name):
"""Delete a blueprint from git"""
branch = request.args.get("branch", "master")
@ -1167,8 +1174,10 @@ def v0_api(api):
else:
return jsonify(status=True)
@api.route("/api/v0/blueprints/workspace", defaults={'blueprint_name': ""}, methods=["DELETE"])
@api.route("/api/v0/blueprints/workspace/<blueprint_name>", methods=["DELETE"])
@crossdomain(origin="*")
@checkparams([("blueprint_name", "", "no blueprint name given")])
def v0_blueprints_delete_workspace(blueprint_name):
"""Delete a blueprint from the workspace"""
branch = request.args.get("branch", "master")
@ -1181,8 +1190,12 @@ def v0_api(api):
else:
return jsonify(status=True)
@api.route("/api/v0/blueprints/undo", defaults={'blueprint_name': "", 'commit': ""}, methods=["POST"])
@api.route("/api/v0/blueprints/undo/<blueprint_name>", defaults={'commit': ""}, methods=["POST"])
@api.route("/api/v0/blueprints/undo/<blueprint_name>/<commit>", methods=["POST"])
@crossdomain(origin="*")
@checkparams([("blueprint_name", "", "no blueprint name given"),
("commit", "", "no commit ID given")])
def v0_blueprints_undo(blueprint_name, commit):
"""Undo changes to a blueprint by reverting to a previous commit."""
branch = request.args.get("branch", "master")
@ -1199,8 +1212,10 @@ def v0_api(api):
else:
return jsonify(status=True)
@api.route("/api/v0/blueprints/tag", defaults={'blueprint_name': ""}, methods=["POST"])
@api.route("/api/v0/blueprints/tag/<blueprint_name>", methods=["POST"])
@crossdomain(origin="*")
@checkparams([("blueprint_name", "", "no blueprint name given")])
def v0_blueprints_tag(blueprint_name):
"""Tag a blueprint's latest blueprint commit as a 'revision'"""
branch = request.args.get("branch", "master")
@ -1213,8 +1228,14 @@ def v0_api(api):
else:
return jsonify(status=True)
@api.route("/api/v0/blueprints/diff", defaults={'blueprint_name': "", 'from_commit': "", 'to_commit': ""})
@api.route("/api/v0/blueprints/diff/<blueprint_name>", defaults={'from_commit': "", 'to_commit': ""})
@api.route("/api/v0/blueprints/diff/<blueprint_name>/<from_commit>", defaults={'to_commit': ""})
@api.route("/api/v0/blueprints/diff/<blueprint_name>/<from_commit>/<to_commit>")
@crossdomain(origin="*")
@checkparams([("blueprint_name", "", "no blueprint name given"),
("from_commit", "", "no from commit ID given"),
("to_commit", "", "no to commit ID given")])
def v0_blueprints_diff(blueprint_name, from_commit, to_commit):
"""Return the differences between two commits of a blueprint"""
branch = request.args.get("branch", "master")
@ -1250,8 +1271,10 @@ def v0_api(api):
diff = recipe_diff(old_blueprint, new_blueprint)
return jsonify(diff=diff)
@api.route("/api/v0/blueprints/freeze", defaults={'blueprint_names': ""})
@api.route("/api/v0/blueprints/freeze/<blueprint_names>")
@crossdomain(origin="*")
@checkparams([("blueprint_names", "", "no blueprint names given")])
def v0_blueprints_freeze(blueprint_names):
"""Return the blueprint with the exact modules and packages selected by depsolve"""
branch = request.args.get("branch", "master")
@ -1303,8 +1326,10 @@ def v0_api(api):
else:
return jsonify(blueprints=blueprints, errors=errors)
@api.route("/api/v0/blueprints/depsolve", defaults={'blueprint_names': ""})
@api.route("/api/v0/blueprints/depsolve/<blueprint_names>")
@crossdomain(origin="*")
@checkparams([("blueprint_names", "", "no blueprint names given")])
def v0_blueprints_depsolve(blueprint_names):
"""Return the dependencies for a blueprint"""
branch = request.args.get("branch", "master")
@ -1377,8 +1402,10 @@ def v0_api(api):
projects = take_limits(available, offset, limit)
return jsonify(projects=projects, offset=offset, limit=limit, total=len(available))
@api.route("/api/v0/projects/info", defaults={'project_names': ""})
@api.route("/api/v0/projects/info/<project_names>")
@crossdomain(origin="*")
@checkparams([("project_names", "", "no project names given")])
def v0_projects_info(project_names):
"""Return detailed information about the listed projects"""
try:
@ -1390,8 +1417,10 @@ def v0_api(api):
return jsonify(projects=projects)
@api.route("/api/v0/projects/depsolve", defaults={'project_names': ""})
@api.route("/api/v0/projects/depsolve/<project_names>")
@crossdomain(origin="*")
@checkparams([("project_names", "", "no project names given")])
def v0_projects_depsolve(project_names):
"""Return detailed information about the listed projects"""
try:
@ -1412,8 +1441,10 @@ def v0_api(api):
sources = sorted([r.id for r in repos])
return jsonify(sources=sources)
@api.route("/api/v0/projects/source/info", defaults={'source_names': ""})
@api.route("/api/v0/projects/source/info/<source_names>")
@crossdomain(origin="*")
@checkparams([("source_names", "", "no source names given")])
def v0_projects_source_info(source_names):
"""Return detailed info about the list of sources"""
out_fmt = request.args.get("format", "json")
@ -1506,8 +1537,10 @@ def v0_api(api):
return jsonify(status=True)
@api.route("/api/v0/projects/source/delete", defaults={'source_name': ""}, methods=["DELETE"])
@api.route("/api/v0/projects/source/delete/<source_name>", methods=["DELETE"])
@crossdomain(origin="*")
@checkparams([("source_name", "", "no source name given")])
def v0_projects_source_delete(source_name):
"""Delete the named source and return a status response"""
system_sources = get_repo_sources("/etc/yum.repos.d/*.repo")
@ -1560,8 +1593,10 @@ def v0_api(api):
modules = take_limits(available, offset, limit)
return jsonify(modules=modules, offset=offset, limit=limit, total=len(available))
@api.route("/api/v0/modules/info", defaults={'module_names': ""})
@api.route("/api/v0/modules/info/<module_names>")
@crossdomain(origin="*")
@checkparams([("module_names", "", "no module names given")])
def v0_modules_info(module_names):
"""Return detailed information about the listed modules"""
try:
@ -1655,8 +1690,10 @@ def v0_api(api):
"""Return the list of failed composes"""
return jsonify(failed=build_status(api.config["COMPOSER_CFG"], "FAILED"))
@api.route("/api/v0/compose/status", defaults={'uuids': ""})
@api.route("/api/v0/compose/status/<uuids>")
@crossdomain(origin="*")
@checkparams([("uuids", "", "no UUIDs given")])
def v0_compose_status(uuids):
"""Return the status of the listed uuids"""
results = []
@ -1667,8 +1704,10 @@ def v0_api(api):
return jsonify(uuids=results)
@api.route("/api/v0/compose/cancel", defaults={'uuid': ""}, methods=["DELETE"])
@api.route("/api/v0/compose/cancel/<uuid>", methods=["DELETE"])
@crossdomain(origin="*")
@checkparams([("uuid", "", "no UUID given")])
def v0_compose_cancel(uuid):
"""Cancel a running compose and delete its results directory"""
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
@ -1685,8 +1724,10 @@ def v0_api(api):
else:
return jsonify(status=True, uuid=uuid)
@api.route("/api/v0/compose/delete", defaults={'uuids': ""}, methods=["DELETE"])
@api.route("/api/v0/compose/delete/<uuids>", methods=["DELETE"])
@crossdomain(origin="*")
@checkparams([("uuids", "", "no UUIDs given")])
def v0_compose_delete(uuids):
"""Delete the compose results for the listed uuids"""
results = []
@ -1706,8 +1747,10 @@ def v0_api(api):
results.append({"uuid":uuid, "status":True})
return jsonify(uuids=results, errors=errors)
@api.route("/api/v0/compose/info", defaults={'uuid': ""})
@api.route("/api/v0/compose/info/<uuid>")
@crossdomain(origin="*")
@checkparams([("uuid", "", "no UUID given")])
def v0_compose_info(uuid):
"""Return detailed info about a compose"""
try:
@ -1717,8 +1760,10 @@ def v0_api(api):
return jsonify(**info)
@api.route("/api/v0/compose/metadata", defaults={'uuid': ""})
@api.route("/api/v0/compose/metadata/<uuid>")
@crossdomain(origin="*")
@checkparams([("uuid","", "no UUID given")])
def v0_compose_metadata(uuid):
"""Return a tar of the metadata for the build"""
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
@ -1732,8 +1777,10 @@ def v0_api(api):
headers=[("Content-Disposition", "attachment; filename=%s-metadata.tar;" % uuid)],
direct_passthrough=True)
@api.route("/api/v0/compose/results", defaults={'uuid': ""})
@api.route("/api/v0/compose/results/<uuid>")
@crossdomain(origin="*")
@checkparams([("uuid","", "no UUID given")])
def v0_compose_results(uuid):
"""Return a tar of the metadata and the results for the build"""
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
@ -1747,8 +1794,10 @@ def v0_api(api):
headers=[("Content-Disposition", "attachment; filename=%s.tar;" % uuid)],
direct_passthrough=True)
@api.route("/api/v0/compose/logs", defaults={'uuid': ""})
@api.route("/api/v0/compose/logs/<uuid>")
@crossdomain(origin="*")
@checkparams([("uuid","", "no UUID given")])
def v0_compose_logs(uuid):
"""Return a tar of the metadata for the build"""
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
@ -1762,8 +1811,10 @@ def v0_api(api):
headers=[("Content-Disposition", "attachment; filename=%s-logs.tar;" % uuid)],
direct_passthrough=True)
@api.route("/api/v0/compose/image", defaults={'uuid': ""})
@api.route("/api/v0/compose/image/<uuid>")
@crossdomain(origin="*")
@checkparams([("uuid","", "no UUID given")])
def v0_compose_image(uuid):
"""Return the output image for the build"""
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
@ -1783,8 +1834,10 @@ def v0_api(api):
# XXX - Will mime type guessing work for all our output?
return send_file(image_path, as_attachment=True, attachment_filename=image_name, add_etags=False)
@api.route("/api/v0/compose/log", defaults={'uuid': ""})
@api.route("/api/v0/compose/log/<uuid>")
@crossdomain(origin="*")
@checkparams([("uuid","", "no UUID given")])
def v0_compose_log_tail(uuid):
"""Return the end of the main anaconda.log, defaults to 1Mbytes"""
try: