parent
a15b8b1b9a
commit
b4bcc61991
@ -0,0 +1,8 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
|
||||
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
|
||||
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-160} \
|
||||
${PYTHON:-python} -m subunit.run discover -t ./ ./glare/tests $LISTOPT $IDOPTION
|
||||
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
@ -0,0 +1,245 @@
|
||||
# optional: after how many files to update progress
|
||||
#show_progress_every: 100
|
||||
|
||||
# optional: plugins directory name
|
||||
#plugins_dir: 'plugins'
|
||||
|
||||
# optional: plugins discovery name pattern
|
||||
plugin_name_pattern: '*.py'
|
||||
|
||||
# optional: terminal escape sequences to display colors
|
||||
#output_colors:
|
||||
# DEFAULT: '\033[0m'
|
||||
# HEADER: '\033[95m'
|
||||
# LOW: '\033[94m'
|
||||
# MEDIUM: '\033[93m'
|
||||
# HIGH: '\033[91m'
|
||||
|
||||
# optional: log format string
|
||||
#log_format: "[%(module)s]\t%(levelname)s\t%(message)s"
|
||||
|
||||
# globs of files which should be analyzed
|
||||
include:
|
||||
- '*.py'
|
||||
- '*.pyw'
|
||||
|
||||
# a list of strings, which if found in the path will cause files to be excluded
|
||||
# for example /tests/ - to remove all all files in tests directory
|
||||
exclude_dirs:
|
||||
- '/tests/'
|
||||
|
||||
profiles:
|
||||
gate:
|
||||
include:
|
||||
|
||||
- any_other_function_with_shell_equals_true
|
||||
- assert_used
|
||||
- blacklist_calls
|
||||
- blacklist_import_func
|
||||
|
||||
# One of the blacklisted imports is the subprocess module. Keystone
|
||||
# has to import the subprocess module in a single module for
|
||||
# eventlet support so in most cases bandit won't be able to detect
|
||||
# that subprocess is even being imported. Also, Bandit's
|
||||
# recommendation is just to check that the use is safe without any
|
||||
# documentation on what safe or unsafe usage is. So this test is
|
||||
# skipped.
|
||||
# - blacklist_imports
|
||||
|
||||
- exec_used
|
||||
|
||||
- execute_with_run_as_root_equals_true
|
||||
|
||||
# - hardcoded_bind_all_interfaces # TODO: enable this test
|
||||
|
||||
# Not working because wordlist/default-passwords file not bundled,
|
||||
# see https://bugs.launchpad.net/bandit/+bug/1451575 :
|
||||
# - hardcoded_password
|
||||
|
||||
# Not used because it's prone to false positives:
|
||||
# - hardcoded_sql_expressions
|
||||
|
||||
# - hardcoded_tmp_directory # TODO: enable this test
|
||||
|
||||
- jinja2_autoescape_false
|
||||
|
||||
- linux_commands_wildcard_injection
|
||||
|
||||
- paramiko_calls
|
||||
|
||||
- password_config_option_not_marked_secret
|
||||
- request_with_no_cert_validation
|
||||
- set_bad_file_permissions
|
||||
- subprocess_popen_with_shell_equals_true
|
||||
# - subprocess_without_shell_equals_true # TODO: enable this test
|
||||
- start_process_with_a_shell
|
||||
# - start_process_with_no_shell # TODO: enable this test
|
||||
- start_process_with_partial_path
|
||||
- ssl_with_bad_defaults
|
||||
- ssl_with_bad_version
|
||||
- ssl_with_no_version
|
||||
# - try_except_pass # TODO: enable this test
|
||||
|
||||
- use_of_mako_templates
|
||||
|
||||
blacklist_calls:
|
||||
bad_name_sets:
|
||||
# - pickle:
|
||||
# qualnames: [pickle.loads, pickle.load, pickle.Unpickler,
|
||||
# cPickle.loads, cPickle.load, cPickle.Unpickler]
|
||||
# message: "Pickle library appears to be in use, possible security issue."
|
||||
# TODO: enable this test
|
||||
- marshal:
|
||||
qualnames: [marshal.load, marshal.loads]
|
||||
message: "Deserialization with the marshal module is possibly dangerous."
|
||||
# - md5:
|
||||
# qualnames: [hashlib.md5, Crypto.Hash.MD2.new, Crypto.Hash.MD4.new, Crypto.Hash.MD5.new, cryptography.hazmat.primitives.hashes.MD5]
|
||||
# message: "Use of insecure MD2, MD4, or MD5 hash function."
|
||||
# TODO: enable this test
|
||||
- mktemp_q:
|
||||
qualnames: [tempfile.mktemp]
|
||||
message: "Use of insecure and deprecated function (mktemp)."
|
||||
- eval:
|
||||
qualnames: [eval]
|
||||
message: "Use of possibly insecure function - consider using safer ast.literal_eval."
|
||||
- mark_safe:
|
||||
names: [mark_safe]
|
||||
message: "Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed."
|
||||
- httpsconnection:
|
||||
qualnames: [httplib.HTTPSConnection]
|
||||
message: "Use of HTTPSConnection does not provide security, see https://wiki.openstack.org/wiki/OSSN/OSSN-0033"
|
||||
- yaml_load:
|
||||
qualnames: [yaml.load]
|
||||
message: "Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load()."
|
||||
- urllib_urlopen:
|
||||
qualnames: [urllib.urlopen, urllib.urlretrieve, urllib.URLopener, urllib.FancyURLopener, urllib2.urlopen, urllib2.Request]
|
||||
message: "Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected."
|
||||
- random:
|
||||
qualnames: [random.random, random.randrange, random.randint, random.choice, random.uniform, random.triangular]
|
||||
message: "Standard pseudo-random generators are not suitable for security/cryptographic purposes."
|
||||
level: "LOW"
|
||||
|
||||
# Most of this is based off of Christian Heimes' work on defusedxml:
|
||||
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
|
||||
|
||||
# TODO(jaegerandi): Enable once defusedxml is in global requirements.
|
||||
#- xml_bad_cElementTree:
|
||||
# qualnames: [xml.etree.cElementTree.parse,
|
||||
# xml.etree.cElementTree.iterparse,
|
||||
# xml.etree.cElementTree.fromstring,
|
||||
# xml.etree.cElementTree.XMLParser]
|
||||
# message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
#- xml_bad_ElementTree:
|
||||
# qualnames: [xml.etree.ElementTree.parse,
|
||||
# xml.etree.ElementTree.iterparse,
|
||||
# xml.etree.ElementTree.fromstring,
|
||||
# xml.etree.ElementTree.XMLParser]
|
||||
# message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
- xml_bad_expatreader:
|
||||
qualnames: [xml.sax.expatreader.create_parser]
|
||||
message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
- xml_bad_expatbuilder:
|
||||
qualnames: [xml.dom.expatbuilder.parse,
|
||||
xml.dom.expatbuilder.parseString]
|
||||
message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
- xml_bad_sax:
|
||||
qualnames: [xml.sax.parse,
|
||||
xml.sax.parseString,
|
||||
xml.sax.make_parser]
|
||||
message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
- xml_bad_minidom:
|
||||
qualnames: [xml.dom.minidom.parse,
|
||||
xml.dom.minidom.parseString]
|
||||
message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
- xml_bad_pulldom:
|
||||
qualnames: [xml.dom.pulldom.parse,
|
||||
xml.dom.pulldom.parseString]
|
||||
message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
- xml_bad_etree:
|
||||
qualnames: [lxml.etree.parse,
|
||||
lxml.etree.fromstring,
|
||||
lxml.etree.RestrictedElement,
|
||||
lxml.etree.GlobalParserTLS,
|
||||
lxml.etree.getDefaultParser,
|
||||
lxml.etree.check_docinfo]
|
||||
message: "Using {func} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {func} with it's defusedxml equivilent function."
|
||||
|
||||
|
||||
shell_injection:
|
||||
# Start a process using the subprocess module, or one of its wrappers.
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call,
|
||||
subprocess.check_output, utils.execute, utils.execute_with_timeout]
|
||||
# Start a process with a function vulnerable to shell injection.
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4,
|
||||
popen2.popen2, popen2.popen3, popen2.popen4, popen2.Popen3,
|
||||
popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
# Start a process with a function that is not vulnerable to shell injection.
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv,os.execve,
|
||||
os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp,
|
||||
os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,
|
||||
os.startfile]
|
||||
|
||||
blacklist_imports:
|
||||
bad_import_sets:
|
||||
- telnet:
|
||||
imports: [telnetlib]
|
||||
level: HIGH
|
||||
message: "Telnet is considered insecure. Use SSH or some other encrypted protocol."
|
||||
- info_libs:
|
||||
imports: [pickle, cPickle, subprocess, Crypto]
|
||||
level: LOW
|
||||
message: "Consider possible security implications associated with {module} module."
|
||||
|
||||
# Most of this is based off of Christian Heimes' work on defusedxml:
|
||||
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
|
||||
|
||||
- xml_libs:
|
||||
imports: [xml.etree.cElementTree,
|
||||
xml.etree.ElementTree,
|
||||
xml.sax.expatreader,
|
||||
xml.sax,
|
||||
xml.dom.expatbuilder,
|
||||
xml.dom.minidom,
|
||||
xml.dom.pulldom,
|
||||
lxml.etree,
|
||||
lxml]
|
||||
message: "Using {module} to parse untrusted XML data is known to be vulnerable to XML attacks. Replace {module} with the equivilent defusedxml package."
|
||||
level: LOW
|
||||
- xml_libs_high:
|
||||
imports: [xmlrpclib]
|
||||
message: "Using {module} to parse untrusted XML data is known to be vulnerable to XML attacks. Use defused.xmlrpc.monkey_patch() function to monkey-patch xmlrpclib and mitigate XML vulnerabilities."
|
||||
level: HIGH
|
||||
|
||||
hardcoded_tmp_directory:
|
||||
tmp_dirs: ['/tmp', '/var/tmp', '/dev/shm']
|
||||
|
||||
hardcoded_password:
|
||||
# Support for full path, relative path and special "%(site_data_dir)s"
|
||||
# substitution (/usr/{local}/share)
|
||||
word_list: "%(site_data_dir)s/wordlist/default-passwords"
|
||||
|
||||
ssl_with_bad_version:
|
||||
bad_protocol_versions:
|
||||
- 'PROTOCOL_SSLv2'
|
||||
- 'SSLv2_METHOD'
|
||||
- 'SSLv23_METHOD'
|
||||
- 'PROTOCOL_SSLv3' # strict option
|
||||
- 'PROTOCOL_TLSv1' # strict option
|
||||
- 'SSLv3_METHOD' # strict option
|
||||
- 'TLSv1_METHOD' # strict option
|
||||
|
||||
password_config_option_not_marked_secret:
|
||||
function_names:
|
||||
- oslo.config.cfg.StrOpt
|
||||
- oslo_config.cfg.StrOpt
|
||||
|
||||
execute_with_run_as_root_equals_true:
|
||||
function_names:
|
||||
- ceilometer.utils.execute
|
||||
- cinder.utils.execute
|
||||
- neutron.agent.linux.utils.execute
|
||||
- nova.utils.execute
|
||||
- nova.utils.trycmd
|
||||
|
||||
try_except_pass:
|
||||
check_typed_exception: True
|
@ -0,0 +1,38 @@
|
||||
# Use this pipeline for no auth - DEFAULT
|
||||
[pipeline:glare-api]
|
||||
pipeline = cors faultwrapper healthcheck versionnegotiation osprofiler unauthenticated-context glarev1api
|
||||
|
||||
# Use this pipeline for keystone auth
|
||||
[pipeline:glare-api-keystone]
|
||||
pipeline = cors faultwrapper healthcheck versionnegotiation osprofiler authtoken context glarev1api
|
||||
|
||||
[app:glarev1api]
|
||||
paste.app_factory = glare.api.v1.router:API.factory
|
||||
|
||||
[filter:healthcheck]
|
||||
paste.filter_factory = oslo_middleware:Healthcheck.factory
|
||||
backends = disable_by_file
|
||||
disable_by_file_path = /etc/glare/healthcheck_disable
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory = glare.api.middleware.version_negotiation:GlareVersionNegotiationFilter.factory
|
||||
|
||||
[filter:faultwrapper]
|
||||
paste.filter_factory = glare.api.middleware.fault:GlareFaultWrapperFilter.factory
|
||||
|
||||
[filter:context]
|
||||
paste.filter_factory = glare.api.middleware.glare_context:ContextMiddleware.factory
|
||||
|
||||
[filter:unauthenticated-context]
|
||||
paste.filter_factory = glare.api.middleware.glare_context:UnauthenticatedContextMiddleware.factory
|
||||
|
||||
[filter:authtoken]
|
||||
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
|
||||
delay_auth_decision = true
|
||||
|
||||
[filter:osprofiler]
|
||||
paste.filter_factory = osprofiler.web:WsgiMiddleware.factory
|
||||
|
||||
[filter:cors]
|
||||
use = egg:oslo.middleware#cors
|
||||
oslo_config_project = glare
|
@ -0,0 +1,25 @@
|
||||
# glare-swift.conf.sample
|
||||
#
|
||||
# This file is an example config file when
|
||||
# multiple swift accounts/backing stores are enabled.
|
||||
#
|
||||
# Specify the reference name in []
|
||||
# For each section, specify the auth_address, user and key.
|
||||
#
|
||||
# WARNING:
|
||||
# * If any of auth_address, user or key is not specified,
|
||||
# the glare's swift store will fail to configure
|
||||
|
||||
[ref1]
|
||||
user = tenant:user1
|
||||
key = key1
|
||||
auth_version = 2
|
||||
auth_address = http://localhost:5000/v2.0
|
||||
|
||||
[ref2]
|
||||
user = project_name:user_name2
|
||||
key = key2
|
||||
user_domain_id = default
|
||||
project_domain_id = default
|
||||
auth_version = 3
|
||||
auth_address = http://localhost:5000/v3
|
@ -0,0 +1,9 @@
|
||||
[DEFAULT]
|
||||
output_file = etc/glare.conf.sample
|
||||
namespace = glare
|
||||
namespace = glance.store
|
||||
namespace = oslo.db
|
||||
namespace = oslo.db.concurrency
|
||||
namespace = keystonemiddleware.auth_token
|
||||
namespace = oslo.log
|
||||
namespace = oslo.middleware.cors
|
@ -0,0 +1,130 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""A middleware that turns exceptions into parsable string.
|
||||
Inspired by Cinder's and Heat't faultwrapper.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import reflection
|
||||
import six
|
||||
import webob
|
||||
|
||||
from glare.common import exception
|
||||
from glare.common import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Fault(object):
|
||||
|
||||
def __init__(self, error):
|
||||
self.error = error
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
serializer = wsgi.JSONResponseSerializer()
|
||||
resp = webob.Response(request=req)
|
||||
default_webob_exc = webob.exc.HTTPInternalServerError()
|
||||
resp.status_code = self.error.get('code', default_webob_exc.code)
|
||||
serializer.default(resp, self.error)
|
||||
return resp
|
||||
|
||||
|
||||
class GlareFaultWrapperFilter(wsgi.Middleware):
|
||||
"""Replace error body with something the client can parse."""
|
||||
error_map = {
|
||||
'BadRequest': webob.exc.HTTPBadRequest,
|
||||
'Unauthorized': webob.exc.HTTPUnauthorized,
|
||||
'Forbidden': webob.exc.HTTPForbidden,
|
||||
'NotFound': webob.exc.HTTPNotFound,
|
||||
'RequestTimeout': webob.exc.HTTPRequestTimeout,
|
||||
'Conflict': webob.exc.HTTPConflict,
|
||||
'Gone': webob.exc.HTTPGone,
|
||||
'PreconditionFailed': webob.exc.HTTPPreconditionFailed,
|
||||
'RequestEntityTooLarge': webob.exc.HTTPRequestEntityTooLarge,
|
||||
'UnsupportedMediaType': webob.exc.HTTPUnsupportedMediaType,
|
||||
'RequestRangeNotSatisfiable': webob.exc.HTTPRequestRangeNotSatisfiable,
|
||||
'Locked': webob.exc.HTTPLocked,
|
||||
'FailedDependency': webob.exc.HTTPFailedDependency,
|
||||
'NotAcceptable': webob.exc.HTTPNotAcceptable,
|
||||
}
|
||||
|
||||
def _map_exception_to_error(self, class_exception):
|
||||
if class_exception == exception.GlareException:
|
||||
return webob.exc.HTTPInternalServerError
|
||||
|
||||
if class_exception.__name__ not in self.error_map:
|
||||
return self._map_exception_to_error(class_exception.__base__)
|
||||
|
||||
return self.error_map[class_exception.__name__]
|
||||
|
||||
def _error(self, ex):
|
||||
|
||||
trace = None
|
||||
traceback_marker = 'Traceback (most recent call last)'
|
||||
webob_exc = None
|
||||
|
||||
ex_type = reflection.get_class_name(ex, fully_qualified=False)
|
||||
|
||||
full_message = six.text_type(ex)
|
||||
if traceback_marker in full_message:
|
||||
message, msg_trace = full_message.split(traceback_marker, 1)
|
||||
message = message.rstrip('\n')
|
||||
msg_trace = traceback_marker + msg_trace
|
||||
else:
|
||||
msg_trace = 'None\n'
|
||||
if sys.exc_info() != (None, None, None):
|
||||
msg_trace = traceback.format_exc()
|
||||
message = full_message
|
||||
|
||||
if isinstance(ex, exception.GlareException):
|
||||
message = ex.message
|
||||
|
||||
if cfg.CONF.debug and not trace:
|
||||
trace = msg_trace
|
||||
|
||||
if not webob_exc:
|
||||
webob_exc = self._map_exception_to_error(ex.__class__)
|
||||
|
||||
error = {
|
||||
'code': webob_exc.code,
|
||||
'title': webob_exc.title,
|
||||
'explanation': webob_exc.explanation,
|
||||
'error': {
|
||||
'message': message,
|
||||
'type': ex_type,
|
||||
'traceback': trace,
|
||||
}
|
||||
}
|
||||
|
||||
# add microversion header is this is not acceptable request
|
||||
if isinstance(ex, exception.InvalidGlobalAPIVersion):
|
||||
error['min_version'] = ex.kwargs['min_ver']
|
||||
error['max_version'] = ex.kwargs['max_ver']
|
||||
|
||||
return error
|
||||
|
||||
def process_request(self, req):
|
||||
try:
|
||||
return req.get_response(self.application)
|
||||
except Exception as exc:
|
||||
LOG.exception(exc)
|
||||
return req.get_response(Fault(self._error(exc)))
|
@ -0,0 +1,131 @@
|
||||
# Copyright 2011-2016 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import webob
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_context import context
|
||||
from oslo_log import log as logging
|
||||
from oslo_middleware import request_id
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from glare.common import policy
|
||||
from glare.common import wsgi
|
||||
from glare.i18n import _
|
||||
|
||||
context_opts = [
|
||||
cfg.BoolOpt('allow_anonymous_access', default=False,
|
||||
help=_('Allow unauthenticated users to access the API with '
|
||||
'read-only privileges. This only applies when using '
|
||||
'ContextMiddleware.'))
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(context_opts)
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestContext(context.RequestContext):
|
||||
"""Stores information about the security context for Glare.
|
||||
|
||||
Stores how the user accesses the system, as well as additional request
|
||||
information.
|
||||
"""
|
||||
|
||||
def __init__(self, service_catalog=None, **kwargs):
|
||||
super(RequestContext, self).__init__(**kwargs)
|
||||
self.service_catalog = service_catalog
|
||||
# check if user is admin using policy file
|
||||
if kwargs.get('is_admin') is None:
|
||||
self.is_admin = policy.check_is_admin(self)
|
||||
|
||||
def to_dict(self):
|
||||
d = super(RequestContext, self).to_dict()
|
||||
d.update({
|
||||
'service_catalog': self.service_catalog,
|
||||
})
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, values):
|
||||
return cls(**values)
|
||||
|
||||
def to_policy_values(self):
|
||||
values = super(RequestContext, self).to_policy_values()
|
||||
values['is_admin'] = self.is_admin
|
||||
values['read_only'] = self.read_only
|
||||
return values
|
||||
|
||||
|
||||
class ContextMiddleware(wsgi.Middleware):
|
||||
|
||||
def __init__(self, app):
|
||||
super(ContextMiddleware, self).__init__(app)
|
||||
|
||||
def process_request(self, req):
|
||||
"""Convert authentication information into a request context
|
||||
|
||||
Generate a RequestContext object from the available
|
||||
authentication headers and store on the 'context' attribute
|
||||
of the req object.
|
||||
|
||||
:param req: wsgi request object that will be given the context object
|
||||
:raises: webob.exc.HTTPUnauthorized: when value of the
|
||||
X-Identity-Status header is not
|
||||
'Confirmed' and anonymous access
|
||||
is disallowed
|
||||
"""
|
||||
if req.headers.get('X-Identity-Status') == 'Confirmed':
|
||||
req.context = self._get_authenticated_context(req)
|
||||
elif CONF.allow_anonymous_access:
|
||||
req.context = self._get_anonymous_context()
|
||||
else:
|
||||
raise webob.exc.HTTPUnauthorized()
|
||||
|
||||
@staticmethod
|
||||
def _get_anonymous_context():
|
||||
"""Anonymous user has only Read-Only grants"""
|
||||
return RequestContext(read_only=True, is_admin=False)
|
||||
|
||||
@staticmethod
|
||||
def _get_authenticated_context(req):
|
||||
headers = req.headers
|
||||
service_catalog = None
|
||||
if headers.get('X-Service-Catalog') is not None:
|
||||
catalog_header = headers.get('X-Service-Catalog')
|
||||
try:
|
||||
service_catalog = jsonutils.loads(catalog_header)
|
||||
except ValueError:
|
||||
raise webob.exc.HTTPInternalServerError(
|
||||
_('Invalid service catalog json.'))
|
||||
kwargs = {
|
||||
'service_catalog': service_catalog,
|
||||
'request_id': req.environ.get(request_id.ENV_REQUEST_ID),
|
||||
}
|
||||
return RequestContext.from_environ(req.environ, **kwargs)
|
||||
|
||||
|
||||
class UnauthenticatedContextMiddleware(wsgi.Middleware):
|
||||
"""Process requests and responses when auth is turned off at all."""
|
||||
|
||||
def process_request(self, req):
|
||||
"""Create a context without an authorized user.
|
||||
|
||||
When glare deployed as public repo everybody is admin
|
||||
without any credentials.
|
||||
"""
|
||||
req.context = RequestContext(is_admin=True)
|
@ -0,0 +1,134 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
A filter middleware that inspects the requested URI for a version string
|
||||
and/or Accept headers and attempts to negotiate an API controller to
|
||||
return
|
||||
"""
|
||||
|
||||
import microversion_parse
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from glare.api.v1 import api_version_request as api_version
|
||||
from glare.api import versions as artifacts_versions
|
||||
from glare.common import exception
|
||||
from glare.common import wsgi
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_version_from_accept(accept_header, vnd_mime_type):
|
||||
"""Try to parse accept header to extract api version
|
||||
|
||||
:param accept_header: accept header
|
||||
:return: version string in the request or None if not specified
|
||||
"""
|
||||
accept = str(accept_header)
|
||||
if accept.startswith(vnd_mime_type):
|
||||
LOG.debug("Using media-type versioning")
|
||||
token_loc = len(vnd_mime_type)
|
||||
return accept[token_loc:]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class GlareVersionNegotiationFilter(wsgi.Middleware):
|
||||
"""Middleware that defines API version in request and redirects it
|
||||
to correct Router.
|
||||
"""
|
||||
|
||||
SERVICE_TYPE = 'artifact'
|
||||
|
||||
def __init__(self, app):
|
||||
super(GlareVersionNegotiationFilter, self).__init__(app)
|
||||
self.vnd_mime_type = 'application/vnd.openstack.artifacts-'
|
||||
|
||||
def process_request(self, req):
|
||||
"""Process api request:
|
||||
1. Define if this is request for available versions or not
|
||||
2. If it is not version request check extract version
|
||||
3. Validate available version and add version info to request
|
||||
"""
|
||||
args = {'method': req.method, 'path': req.path, 'accept': req.accept}
|
||||
LOG.debug("Determining version of request: %(method)s %(path)s "
|
||||
"Accept: %(accept)s", args)
|
||||
|
||||
# determine if this is request for versions
|
||||
if req.path_info in ('/versions', '/'):
|
||||
is_multi = req.path_info == '/'
|
||||
return artifacts_versions.Controller.index(
|
||||
req, is_multi=is_multi)
|
||||
|
||||
# determine api version from request
|
||||
req_version = get_version_from_accept(req.accept, self.vnd_mime_type)
|
||||
if req_version is None:
|
||||
# determine api version for v0.1 from url
|
||||
if req.path_info_peek() == 'v0.1':
|
||||
req_version = 'v0.1'
|
||||
else:
|
||||
# determine api version from microversion header
|
||||
LOG.debug("Determine version from microversion header.")
|
||||
req_version = microversion_parse.get_version(
|
||||
req.headers, service_type=self.SERVICE_TYPE)
|
||||
|
||||
# validate versions and add version info to request
|
||||
if req_version == 'v0.1':
|
||||
req.environ['api.version'] = 0.1
|
||||
else:
|
||||
# validate microversions header
|
||||
req.api_version_request = self._get_api_version_request(
|
||||
req_version)
|
||||
req_version = req.api_version_request.get_string()
|
||||
|
||||
LOG.debug("Matched version: %s", req_version)
|
||||
LOG.debug('new path %s', req.path_info)
|
||||
|
||||
@staticmethod
|
||||
def _get_api_version_request(req_version):
|
||||
"""Set API version for request based on the version header string."""
|
||||
if req_version is None:
|
||||
LOG.debug("No API version in request header. Use default version.")
|
||||
cur_ver = api_version.APIVersionRequest.default_version()
|
||||
elif req_version == 'latest':
|
||||
# 'latest' is a special keyword which is equivalent to
|
||||
# requesting the maximum version of the API supported
|
||||
cur_ver = api_version.APIVersionRequest.max_version()
|
||||
else:
|
||||
cur_ver = api_version.APIVersionRequest(req_version)
|
||||
|
||||
# Check that the version requested is within the global
|
||||
# minimum/maximum of supported API versions
|
||||
if not cur_ver.matches(cur_ver.min_version(), cur_ver.max_version()):
|
||||
raise exception.InvalidGlobalAPIVersion(
|
||||
req_ver=cur_ver.get_string(),
|
||||
min_ver=cur_ver.min_version().get_string(),
|
||||
max_ver=cur_ver.max_version().get_string())
|
||||
return cur_ver
|
||||
|
||||
def process_response(self, response):
|
||||
if hasattr(response, 'headers'):
|
||||
request = response.request
|
||||
if hasattr(request, 'api_version_request'):
|
||||
api_header_name = microversion_parse.STANDARD_HEADER
|
||||
response.headers[api_header_name] = (
|
||||
self.SERVICE_TYPE + ' ' +
|
||||
request.api_version_request.get_string())
|
||||
response.headers.add('Vary', api_header_name)
|
||||
|
||||
return response
|
@ -0,0 +1,123 @@
|
||||
# Copyright 2016 Openstack Foundation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
|
||||
from glare.common import exception
|
||||
from glare.i18n import _
|
||||
|
||||
|
||||
REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
|
||||
* 1.0 - First stable API version that supports microversion. If API version
|
||||
is not specified in the request then API v1.0 is used as default API
|
||||
version.
|
||||
"""
|
||||
|
||||
|
||||
class APIVersionRequest(object):
|
||||
"""This class represents an API Version Request with convenience
|
||||
methods for manipulation and comparison of version
|
||||
numbers that we need to do to implement microversions.
|
||||
"""
|
||||
|
||||
_MIN_API_VERSION = "1.0"
|
||||
_MAX_API_VERSION = "1.0"
|
||||
_DEFAULT_API_VERSION = "1.0"
|
||||
|
||||
def __init__(self, version_string):
|
||||
"""Create an API version request object.
|
||||
|
||||
:param version_string: String representation of APIVersionRequest.
|
||||
Correct format is 'X.Y', where 'X' and 'Y' are int values.
|
||||
"""
|
||||
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$", version_string)
|
||||
if match:
|
||||
self.ver_major = int(match.group(1))
|
||||
self.ver_minor = int(match.group(2))
|
||||
else:
|
||||
msg = _("API version string %s is not valid. "
|
||||
"Cannot determine API version.") % version_string
|
||||
raise exception.BadRequest(msg)
|
||||
|
||||
def __str__(self):
|
||||
"""Debug/Logging representation of object."""
|
||||
return ("API Version Request Major: %s, Minor: %s"
|
||||
% (self.ver_major, self.ver_minor))
|
||||
|
||||
def _format_type_error(self, other):
|
||||
return TypeError(_("'%(other)s' should be an instance of '%(cls)s'") %
|
||||
{"other": other, "cls": self.__class__})
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, APIVersionRequest):
|
||||
raise self._format_type_error(other)
|
||||
|
||||
return ((self.ver_major, self.ver_minor) <
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, APIVersionRequest):
|
||||
raise self._format_type_error(other)
|
||||
|
||||
return ((self.ver_major, self.ver_minor) ==
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, APIVersionRequest):
|
||||
raise self._format_type_error(other)
|
||||
|
||||
return ((self.ver_major, self.ver_minor) >
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __le__(self, other):
|
||||
return self < other or self == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self > other or self == other
|
||||
|
||||
def matches(self, min_version, max_version):
|
||||
"""Returns whether the version object represents a version
|
||||
greater than or equal to the minimum version and less than
|
||||
or equal to the maximum version.
|
||||
|
||||
@param min_version: Minimum acceptable version.
|
||||
@param max_version: Maximum acceptable version.
|
||||
@returns: boolean
|
||||
"""
|
||||
return min_version <= self <= max_version
|
||||
|
||||
def get_string(self):
|
||||
"""Converts object to string representation which is used to create
|
||||
an APIVersionRequest object results in the same version request.
|
||||
"""
|
||||
return "%s.%s" % (self.ver_major, self.ver_minor)
|
||||
|
||||
@classmethod
|
||||
def min_version(cls):
|
||||
"""Minimal allowed api version"""
|
||||
return APIVersionRequest(cls._MIN_API_VERSION)
|
||||
|
||||
@classmethod
|
||||
def max_version(cls):
|
||||
"""Maximal allowed api version"""
|
||||
return APIVersionRequest(cls._MAX_API_VERSION)
|
||||
|
||||
@classmethod
|
||||
def default_version(cls):
|
||||
"""Default api version if no version in request"""
|
||||
return APIVersionRequest(cls._DEFAULT_API_VERSION)
|
@ -0,0 +1,169 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
|
||||
from glare.api.v1 import api_version_request as api_version
|
||||
from glare.common import exception as exc
|
||||
from glare.i18n import _
|
||||
|
||||
|
||||
class VersionedMethod(object):
|
||||
|
||||
def __init__(self, name, start_version, end_version, func):
|
||||
"""Versioning information for a single method
|
||||
:param name: Name of the method
|
||||
:param start_version: Minimum acceptable version
|
||||
:param end_version: Maximum acceptable_version
|
||||
:param func: Method to call
|
||||
Minimum and maximums are inclusive
|
||||
"""
|
||||
self.name = name
|
||||
self.start_version = start_version
|
||||
self.end_version = end_version
|
||||
self.func = func
|
||||
|
||||
def __str__(self):
|
||||
return ("Version Method %s: min: %s, max: %s"
|
||||
% (self.name, self.start_version, self.end_version))
|
||||
|
||||
|
||||
class VersionedResource(object):
|
||||
"""Versioned mixin that provides ability to define versioned methods and
|
||||
return appropriate methods based on user request
|
||||
"""
|
||||
|
||||
# prefix for all versioned methods in class
|
||||
VER_METHODS_ATTR_PREFIX = 'versioned_methods_'
|
||||
|
||||
@staticmethod
|
||||
def check_for_versions_intersection(func_list):
|
||||
"""Determines whether function list contains version intervals
|
||||
intersections or not. General algorithm:
|
||||
https://en.wikipedia.org/wiki/Intersection_algorithm
|
||||
:param func_list: list of VersionedMethod objects
|
||||
:return: boolean
|
||||
"""
|
||||
pairs = []
|
||||
counter = 0
|
||||
for f in func_list:
|
||||
pairs.append((f.start_version, 1, f))
|
||||
pairs.append((f.end_version, -1, f))
|
||||
|
||||
def compare(x):
|
||||
return x[0]
|
||||
|
||||
pairs.sort(key=compare)
|
||||
for p in pairs:
|
||||
counter += p[1]
|
||||
if counter > 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def supported_versions(cls, min_ver, max_ver=None):
|
||||
"""Decorator for versioning api methods.
|
||||
|
||||
Add the decorator to any method which takes a request object
|
||||
as the first parameter and belongs to a class which inherits from
|
||||
wsgi.Controller. The implementation inspired by Nova.
|
||||
|
||||
:param min_ver: string representing minimum version
|
||||
:param max_ver: optional string representing maximum version
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
obj_min_ver = api_version.APIVersionRequest(min_ver)
|
||||
if max_ver:
|
||||
obj_max_ver = api_version.APIVersionRequest(max_ver)
|
||||
else:
|
||||
obj_max_ver = api_version.APIVersionRequest.max_version()
|
||||
|
||||
# Add to list of versioned methods registered
|
||||
func_name = f.__name__
|
||||
new_func = VersionedMethod(func_name, obj_min_ver, obj_max_ver, f)
|
||||
|
||||
versioned_attr = cls.VER_METHODS_ATTR_PREFIX + cls.__name__
|
||||
func_dict = getattr(cls, versioned_attr, {})
|
||||
if not func_dict:
|
||||
setattr(cls, versioned_attr, func_dict)
|
||||
|
||||
func_list = func_dict.get(func_name, [])
|
||||
if not func_list:
|
||||
func_dict[func_name] = func_list
|
||||
func_list.append(new_func)
|
||||
|
||||
# Ensure the list is sorted by minimum version (reversed)
|
||||
# so later when we work through the list in order we find
|
||||
# the method which has the latest version which supports
|
||||
# the version requested.
|
||||
is_intersect = cls.check_for_versions_intersection(
|
||||
func_list)
|
||||
|
||||
if is_intersect:
|
||||
raise exc.ApiVersionsIntersect(
|
||||
name=new_func.name,
|
||||
min_ver=new_func.start_version,
|
||||
max_ver=new_func.end_version,
|
||||
)
|
||||
|
||||
func_list.sort(key=lambda vf: vf.start_version, reverse=True)
|
||||
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
def __getattribute__(self, key):
|
||||
def version_select(*args, **kwargs):
|
||||
"""Look for the method which matches the name supplied and version
|
||||
constraints and calls it with the supplied arguments.
|
||||
:returns: Returns the result of the method called
|
||||
:raises: VersionNotFoundForAPIMethod if there is no method which
|
||||
matches the name and version constraints
|
||||
"""
|
||||
# versioning is used in 3 classes: request deserializer and
|
||||
# controller have request as first argument
|
||||
# response serializer has response as first argument
|
||||
# we must respect all three cases
|
||||
if hasattr(args[0], 'api_version_request'):
|
||||
ver = args[0].api_version_request
|
||||
elif hasattr(args[0], 'request'):
|
||||
ver = args[0].request.api_version_request
|
||||
else:
|
||||
raise exc.VersionNotFoundForAPIMethod(
|
||||
message=_("Api version not found in the request."))
|
||||
|
||||
func_list = self.versioned_methods[key]
|
||||
for func in func_list:
|
||||
if ver.matches(func.start_version, func.end_version):
|
||||
# Update the version_select wrapper function so
|
||||
# other decorator attributes like wsgi.response
|
||||
# are still respected.
|
||||
functools.update_wrapper(version_select, func.func)
|
||||
return func.func(self, *args, **kwargs)
|
||||
|
||||
# No version match
|
||||
raise exc.VersionNotFoundForAPIMethod(version=ver)
|
||||
|
||||
class_obj = object.__getattribute__(self, '__class__')
|
||||
prefix = object.__getattribute__(self, 'VER_METHODS_ATTR_PREFIX')
|
||||
attr_name = prefix + object.__getattribute__(class_obj, '__name__')
|
||||
try:
|
||||
if key in object.__getattribute__(self, attr_name):
|
||||
return version_select
|
||||
except AttributeError:
|
||||
# No versioning on this class
|
||||
pass
|
||||
|
||||
return object.__getattribute__(self, key)
|
@ -0,0 +1,484 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""WSGI Resource definition for Glare. Defines Glare API and serialization/
|
||||
deserialization of incoming requests."""
|
||||
|
||||
import json
|
||||
import jsonpatch
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
from six.moves import http_client
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from glare.api.v1 import api_versioning
|
||||
from glare.common import exception as exc
|
||||
from glare.common import wsgi
|
||||
from glare import engine
|
||||
from glare.i18n import _, _LI
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
list_configs = [
|
||||
cfg.IntOpt('default_api_limit', default=25,
|
||||
help=_('Default value for the number of items returned by a '
|
||||
'request if not specified explicitly in the request')),
|
||||
cfg.IntOpt('max_api_limit', default=1000,
|
||||
help=_('Maximum permissible number of items that could be '
|
||||
'returned by a request')),
|
||||
]
|
||||
|
||||
CONF.register_opts(list_configs)
|
||||
|
||||
supported_versions = api_versioning.VersionedResource.supported_versions
|
||||
|
||||
|
||||
class RequestDeserializer(api_versioning.VersionedResource,
|
||||
wsgi.JSONRequestDeserializer):
|
||||
"""Glare deserializer for incoming webop Requests.
|
||||
Deserializer converts incoming request into bunch of python primitives.
|
||||
So other components doesn't work with requests at all. Deserializer also
|
||||
executes primary API validation without any knowledge about Artifact
|
||||
structure.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _get_content_type(req, expected=None):
|
||||
"""Determine content type of the request body."""
|
||||
if "Content-Type" not in req.headers:
|
||||
msg = _("Content-Type must be specified.")
|
||||
LOG.error(msg)
|
||||
raise exc.BadRequest(msg)
|
||||
|
||||
content_type = req.content_type
|
||||
if expected is not None and content_type not in expected:
|
||||
msg = (_('Invalid content type: %(ct)s. Expected: %(exp)s') %
|
||||
{'ct': content_type, 'exp': ', '.join(expected)})
|
||||
raise exc.UnsupportedMediaType(message=msg)
|
||||
|
||||
return content_type
|
||||
|
||||
def _get_request_body(self, req):
|
||||
return self.from_json(req.body)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def create(self, req):
|
||||
self._get_content_type(req, expected=['application/json'])
|
||||
body = self._get_request_body(req)
|
||||
if not isinstance(body, dict):
|
||||
msg = _("Dictionary expected as body value. Got %s.") % type(body)
|
||||
raise exc.BadRequest(msg)
|
||||
return {'values': body}
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def list(self, req):
|
||||
params = req.params.copy()
|
||||
marker = params.pop('marker', None)
|
||||
query_params = {}
|
||||
# step 1 - apply marker to query if exists
|
||||
if marker is not None:
|
||||
query_params['marker'] = marker
|
||||
|
||||
# step 2 - apply limit (if exists OR setup default limit)
|
||||
limit = params.pop('limit', CONF.default_api_limit)
|
||||
try:
|
||||
limit = int(limit)
|
||||
except ValueError:
|
||||
msg = _("Limit param must be an integer.")
|
||||
raise exc.BadRequest(message=msg)
|
||||
if limit < 0:
|
||||
msg = _("Limit param must be positive.")
|
||||
raise exc.BadRequest(message=msg)
|
||||
query_params['limit'] = min(CONF.max_api_limit, limit)
|
||||
|
||||
# step 3 - parse sort parameters
|
||||
if 'sort' in params:
|
||||
sort = []
|
||||
for sort_param in params.pop('sort').strip().split(','):
|
||||
key, _sep, direction = sort_param.partition(':')
|
||||
if direction and direction not in ('asc', 'desc'):
|
||||
raise exc.BadRequest('Sort direction must be one of '
|
||||
'["asc", "desc"]. Got %s direction'
|
||||
% direction)
|
||||
sort.append((key, direction or 'desc'))
|
||||
query_params['sort'] = sort
|
||||
|
||||
query_params['filters'] = params
|
||||
return query_params
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def update(self, req):
|
||||
self._get_content_type(
|
||||
req, expected=['application/json-patch+json'])
|
||||
body = self._get_request_body(req)
|
||||
patch = jsonpatch.JsonPatch(body)
|
||||
try:
|
||||
# Initially patch object doesn't validate input. It's only checked
|
||||
# we call get operation on each method
|
||||
map(patch._get_operation, patch.patch)
|
||||
except (jsonpatch.InvalidJsonPatch, TypeError):
|
||||
msg = _("Json Patch body is malformed")
|
||||
raise exc.BadRequest(msg)
|
||||
for patch_item in body:
|
||||
if patch_item['path'] == '/tags':
|
||||
msg = _("Cannot modify artifact tags with PATCH "
|
||||
"request. Use special Tag API for that.")
|
||||
raise exc.BadRequest(msg)
|
||||
return {'patch': patch}
|
||||
|
||||
def _deserialize_blob(self, req):
|
||||
content_type = self._get_content_type(req)
|
||||
if content_type == ('application/vnd+openstack.glare-custom-location'
|
||||
'+json'):
|
||||
data = self._get_request_body(req)['url']
|
||||
else:
|
||||
data = req.body_file
|
||||
return {'data': data, 'content_type': content_type}
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def upload_blob(self, req):
|
||||
return self._deserialize_blob(req)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def upload_blob_dict(self, req):
|
||||
return self._deserialize_blob(req)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def set_tags(self, req):
|
||||
self._get_content_type(req, expected=['application/json'])
|
||||
body = self._get_request_body(req)
|
||||
|
||||
if 'tags' not in body:
|
||||
msg = _("Tag list must be in the body of request.")
|
||||
raise exc.BadRequest(msg)
|
||||
|
||||
return {'tag_list': body['tags']}
|
||||
|
||||
|
||||
def log_request_progress(f):
|
||||
def log_decorator(self, req, *args, **kwargs):
|
||||
LOG.debug("Request %(request_id)s for %(api_method)s successfully "
|
||||
"deserialized. Pass request parameters to Engine",
|
||||
{'request_id': req.context.request_id,
|
||||
'api_method': f.__name__})
|
||||
result = f(self, req, *args, **kwargs)
|
||||
LOG.info(_LI(
|
||||
"Request %(request_id)s for artifact %(api_method)s "
|
||||
"successfully executed."), {'request_id': req.context.request_id,
|
||||
'api_method': f.__name__})
|
||||
return result
|
||||
return log_decorator
|
||||
|
||||
|
||||
class ArtifactsController(api_versioning.VersionedResource):
|
||||
"""API controller for Glare Artifacts.
|
||||
Artifact Controller prepares incoming data for Glare Engine and redirects
|
||||
data to appropriate engine method (so only controller is working with
|
||||
Engine. Once the data returned from Engine Controller returns data
|
||||
in appropriate format for Response Serializer.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.engine = engine.Engine()
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def list_type_schemas(self, req):
|
||||
type_schemas = self.engine.list_type_schemas(req.context)
|
||||
return type_schemas
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def show_type_schema(self, req, type_name):
|
||||
type_schema = self.engine.show_type_schema(req.context, type_name)
|
||||
return {type_name: type_schema}
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def create(self, req, type_name, values):
|
||||
"""Create artifact record in Glare.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param values: dict with artifact fields {field_name: field_value}
|
||||
:return definition of created artifact
|
||||
"""
|
||||
return self.engine.create(req.context, type_name, values)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def update(self, req, type_name, artifact_id, patch):
|
||||
"""Update artifact record in Glare.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of artifact to update
|
||||
:param patch: json patch with artifact changes
|
||||
:return definition of updated artifact
|
||||
"""
|
||||
return self.engine.update(req.context, type_name, artifact_id, patch)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def delete(self, req, type_name, artifact_id):
|
||||
"""Delete artifact from Glare
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of artifact to delete
|
||||
"""
|
||||
return self.engine.delete(req.context, type_name, artifact_id)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def show(self, req, type_name, artifact_id):
|
||||
"""Show detailed artifact info
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of artifact to show
|
||||
:return: definition of requested artifact
|
||||
"""
|
||||
return self.engine.get(req.context, type_name, artifact_id)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def list(self, req, type_name, filters, marker=None, limit=None,
|
||||
sort=None):
|
||||
"""List available artifacts
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param filters: filters that need to be applied to artifact
|
||||
:param marker: the artifact that considered as begin of the list
|
||||
so all artifacts before marker (including marker itself) will not be
|
||||
added to artifact list
|
||||
:param limit: maximum number of items in list
|
||||
:param sort: sorting options
|
||||
:return: list of artifacts
|
||||
"""
|
||||
artifacts = self.engine.list(req.context, type_name, filters, marker,
|
||||
limit, sort)
|
||||
result = {'artifacts': artifacts,
|
||||
'type_name': type_name}
|
||||
if len(artifacts) != 0 and len(artifacts) == limit:
|
||||
result['next_marker'] = artifacts[-1]['id']
|
||||
return result
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def upload_blob(self, req, type_name, artifact_id, field_name, data,
|
||||
content_type):
|
||||
"""Upload blob into Glare repo
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of Artifact to reactivate
|
||||
:param field_name: name of blob field in artifact
|
||||
:param data: Artifact payload
|
||||
:param content_type: data content-type
|
||||
"""
|
||||
if content_type == ('application/vnd+openstack.glare-custom-location'
|
||||
'+json'):
|
||||
return self.engine.add_blob_location(
|
||||
req.context, type_name, artifact_id, field_name, data)
|
||||
else:
|
||||
return self.engine.upload_blob(req.context, type_name, artifact_id,
|
||||
field_name, data, content_type)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def upload_blob_dict(self, req, type_name, artifact_id, field_name, data,
|
||||
blob_key, content_type):
|
||||
"""Upload blob into Glare repo
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of Artifact to reactivate
|
||||
:param field_name: name of blob field in artifact
|
||||
:param data: Artifact payload
|
||||
:param content_type: data content-type
|
||||
:param blob_key: blob key in dict
|
||||
"""
|
||||
if content_type == ('application/vnd+openstack.glare-custom-location'
|
||||
'+json'):
|
||||
return self.engine.add_blob_dict_location(
|
||||
req.context, type_name, artifact_id,
|
||||
field_name, blob_key, str(data))
|
||||
else:
|
||||
return self.engine.upload_blob_dict(
|
||||
req.context, type_name, artifact_id,
|
||||
field_name, blob_key, data, content_type)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def download_blob(self, req, type_name, artifact_id, field_name):
|
||||
"""Download blob data from Artifact
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of Artifact to reactivate
|
||||
:param field_name: name of blob field in artifact
|
||||
:return: iterator that returns blob data
|
||||
"""
|
||||
data, meta = self.engine.download_blob(req.context, type_name,
|
||||
artifact_id, field_name)
|
||||
result = {'data': data, 'meta': meta}
|
||||
return result
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def download_blob_dict(self, req, type_name, artifact_id,
|
||||
field_name, blob_key):
|
||||
"""Download blob data from Artifact
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of Artifact to reactivate
|
||||
:param field_name: name of blob field in artifact
|
||||
:param blob_key: name of Dict of blobs (optional)
|
||||
:return: iterator that returns blob data
|
||||
"""
|
||||
data, meta = self.engine.download_blob_dict(
|
||||
req.context, type_name, artifact_id, field_name, blob_key)
|
||||
result = {'data': data, 'meta': meta}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _tag_body_resp(af):
|
||||
return {'tags': af['tags']}
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def get_tags(self, req, type_name, artifact_id):
|
||||
return self._tag_body_resp(self.engine.get(
|
||||
req.context, type_name, artifact_id))
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def set_tags(self, req, type_name, artifact_id, tag_list):
|
||||
patch = [{'op': 'replace', 'path': '/tags', 'value': tag_list}]
|
||||
patch = jsonpatch.JsonPatch(patch)
|
||||
return self._tag_body_resp(self.engine.update(
|
||||
req.context, type_name, artifact_id, patch))
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def delete_tags(self, req, type_name, artifact_id):
|
||||
patch = [{'op': 'replace', 'path': '/tags', 'value': []}]
|
||||
patch = jsonpatch.JsonPatch(patch)
|
||||
self.engine.update(req.context, type_name, artifact_id, patch)
|
||||
|
||||
|
||||
class ResponseSerializer(api_versioning.VersionedResource,
|
||||
wsgi.JSONResponseSerializer):
|
||||
"""Glare Response Serializer converts data received from Glare Engine
|
||||
(it consists from plain data types - dict, int, string, file descriptors,
|
||||
etc) to WSGI Requests. It also specifies proper response status and
|
||||
content type as specified by API design.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _prepare_json_response(response, result,
|
||||
content_type='application/json'):
|
||||
body = json.dumps(result, ensure_ascii=False)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = content_type
|
||||
|
||||
def list_type_schemas(self, response, type_schemas):
|
||||
self._prepare_json_response(response,
|
||||
{'schemas': type_schemas},
|
||||
content_type='application/schema+json')
|
||||
|
||||
def show_type_schema(self, response, type_schema):
|
||||
self._prepare_json_response(response,
|
||||
{'schemas': type_schema},
|
||||
content_type='application/schema+json')
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def list_schemas(self, response, type_list):
|
||||
self._prepare_json_response(response, {'types': type_list})
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def create(self, response, artifact):
|
||||
self._prepare_json_response(response, artifact)
|
||||
response.status_int = http_client.CREATED
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def show(self, response, artifact):
|
||||
self._prepare_json_response(response, artifact)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def update(self, response, artifact):
|
||||
self._prepare_json_response(response, artifact)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def list(self, response, af_list):
|
||||
params = dict(response.request.params)
|
||||
params.pop('marker', None)
|
||||
query = urlparse.urlencode(params)
|
||||
type_name = af_list['type_name']
|
||||
body = {
|
||||
type_name: af_list['artifacts'],
|
||||
'first': '/artifacts/%s' % type_name,
|
||||
'schema': '/schemas/%s' % type_name,
|
||||
}
|
||||
if query:
|
||||
body['first'] = '%s?%s' % (body['first'], query)
|
||||
if 'next_marker' in af_list:
|
||||
params['marker'] = af_list['next_marker']
|
||||
next_query = urlparse.urlencode(params)
|
||||
body['next'] = '/artifacts/%s?%s' % (type_name, next_query)
|
||||
response.unicode_body = six.text_type(json.dumps(body,
|
||||
ensure_ascii=False))
|
||||
response.content_type = 'application/json'
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
def delete(self, response, result):
|
||||