diff --git a/kinto/core/cornice/__init__.py b/kinto/core/cornice/__init__.py index 4e4d9dd4c..b7f51618f 100644 --- a/kinto/core/cornice/__init__.py +++ b/kinto/core/cornice/__init__.py @@ -2,17 +2,11 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import logging -from functools import partial from pyramid.events import NewRequest -from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound -from pyramid.security import NO_PERMISSION_REQUIRED -from pyramid.settings import asbool, aslist from kinto.core.cornice.errors import Errors # NOQA from kinto.core.cornice.pyramidhook import ( - handle_exceptions, - register_resource_views, register_service_views, wrap_request, ) @@ -21,49 +15,7 @@ from kinto.core.cornice.util import ContentTypePredicate, current_service -logger = logging.getLogger("cornice") - - -def set_localizer_for_languages(event, available_languages, default_locale_name): - """ - Sets the current locale based on the incoming Accept-Language header, if - present, and sets a localizer attribute on the request object based on - the current locale. - - To be used as an event handler, this function needs to be partially applied - with the available_languages and default_locale_name arguments. The - resulting function will be an event handler which takes an event object as - its only argument. - """ - request = event.request - if request.accept_language: - accepted = request.accept_language.lookup(available_languages, default=default_locale_name) - request._LOCALE_ = accepted - - -def setup_localization(config): - """ - Setup localization based on the available_languages and - pyramid.default_locale_name settings. - - These settings are named after suggestions from the "Internationalization - and Localization" section of the Pyramid documentation. - """ - try: - config.add_translation_dirs("colander:locale/") - settings = config.get_settings() - available_languages = aslist(settings["available_languages"]) - default_locale_name = settings.get("pyramid.default_locale_name", "en") - set_localizer = partial( - set_localizer_for_languages, - available_languages=available_languages, - default_locale_name=default_locale_name, - ) - config.add_subscriber(set_localizer, NewRequest) - except ImportError: # pragma: no cover - # add_translation_dirs raises an ImportError if colander is not - # installed - pass +logger = logging.getLogger("kinto.core.cornice") def includeme(config): @@ -71,23 +23,8 @@ def includeme(config): # attributes required to maintain services config.registry.cornice_services = {} - settings = config.get_settings() - - # localization request subscriber must be set before first call - # for request.localizer (in wrap_request) - if settings.get("available_languages"): - setup_localization(config) - config.add_directive("add_cornice_service", register_service_views) - config.add_directive("add_cornice_resource", register_resource_views) config.add_subscriber(wrap_request, NewRequest) config.add_renderer("cornicejson", CorniceRenderer()) config.add_view_predicate("content_type", ContentTypePredicate) config.add_request_method(current_service, reify=True) - - if asbool(settings.get("handle_exceptions", True)): - config.add_view(handle_exceptions, context=Exception, permission=NO_PERMISSION_REQUIRED) - config.add_view(handle_exceptions, context=HTTPNotFound, permission=NO_PERMISSION_REQUIRED) - config.add_view( - handle_exceptions, context=HTTPForbidden, permission=NO_PERMISSION_REQUIRED - ) diff --git a/kinto/core/cornice/cors.py b/kinto/core/cornice/cors.py index 1f5073e89..d1eb91067 100644 --- a/kinto/core/cornice/cors.py +++ b/kinto/core/cornice/cors.py @@ -4,12 +4,9 @@ import fnmatch import functools -from pyramid.settings import asbool - CORS_PARAMETERS = ( "cors_headers", - "cors_enabled", "cors_origins", "cors_credentials", "cors_max_age", @@ -95,11 +92,7 @@ def ensure_origin(service, request, response=None, **kwargs): origin = request.headers.get("Origin") if not origin: - always_cors = asbool(request.registry.settings.get("cornice.always_cors")) - # With this setting, if the service origins has "*", then - # always return CORS headers. - origins = getattr(service, "cors_origins", []) - if always_cors and "*" in origins: + if "*" in service.cors_origins: origin = "*" if origin: diff --git a/kinto/core/cornice/errors.py b/kinto/core/cornice/errors.py index 746ea723b..3d4ecddaf 100644 --- a/kinto/core/cornice/errors.py +++ b/kinto/core/cornice/errors.py @@ -1,40 +1,18 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -import json - -from pyramid.i18n import TranslationString class Errors(list): """Holds Request errors""" - def __init__(self, status=400, localizer=None): + def __init__(self, status=400): self.status = status - self.localizer = localizer - super(Errors, self).__init__() + super().__init__() def add(self, location, name=None, description=None, **kw): """Registers a new error.""" allowed = ("body", "querystring", "url", "header", "path", "cookies", "method") if location != "" and location not in allowed: raise ValueError("%r not in %s" % (location, allowed)) - - if isinstance(description, TranslationString) and self.localizer: - description = self.localizer.translate(description) - self.append(dict(location=location, name=name, description=description, **kw)) - - @classmethod - def from_json(cls, string): - """Transforms a json string into an `Errors` instance""" - obj = json.loads(string.decode()) - return Errors.from_list(obj.get("errors", [])) - - @classmethod - def from_list(cls, obj): - """Transforms a python list into an `Errors` instance""" - errors = Errors() - for error in obj: - errors.add(**error) - return errors diff --git a/kinto/core/cornice/pyramidhook.py b/kinto/core/cornice/pyramidhook.py index 126d09f7c..47e3df668 100644 --- a/kinto/core/cornice/pyramidhook.py +++ b/kinto/core/cornice/pyramidhook.py @@ -7,7 +7,6 @@ from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import ( - HTTPException, HTTPMethodNotAllowed, HTTPNotAcceptable, HTTPUnsupportedMediaType, @@ -25,7 +24,6 @@ from kinto.core.cornice.util import ( content_type_matches, current_service, - is_string, match_accept_header, match_content_type_header, to_list, @@ -107,32 +105,13 @@ def _fallback_view(request): def apply_filters(request, response): if request.matched_route is not None: - # do some sanity checking on the response using filters + # do some sanity checking on the response service = current_service(request) if service is not None: - kwargs, ob = getattr(request, "cornice_args", ({}, None)) - for _filter in kwargs.get("filters", []): - if is_string(_filter) and ob is not None: - _filter = getattr(ob, _filter) - try: - response = _filter(response, request) - except TypeError: - response = _filter(response) - if service.cors_enabled: - apply_cors_post_request(service, request, response) - + apply_cors_post_request(service, request, response) return response -def handle_exceptions(exc, request): - # At this stage, the checks done by the validators had been removed because - # a new response started (the exception), so we need to do that again. - if not isinstance(exc, HTTPException): - raise - request.info["cors_checked"] = False - return apply_filters(request, exc) - - def add_nosniff_header(request, response): """IE has some rather unfortunately content-type-sniffing behaviour that can be used to trigger XSS attacks via a JSON API, as described here: @@ -184,7 +163,7 @@ def register_service_views(config, service): # before doing anything else, register a view for the OPTIONS method # if we need to - if service.cors_enabled and "OPTIONS" not in service.defined_methods: + if "OPTIONS" not in service.defined_methods: service.add_view( "options", view=get_cors_preflight_view(service), permission=NO_PERMISSION_REQUIRED ) @@ -195,7 +174,6 @@ def register_service_views(config, service): # Cornice-specific arguments that pyramid does not know about cornice_parameters = ( - "filters", "validators", "schema", "klass", @@ -237,9 +215,7 @@ def register_service_views(config, service): args[item] = copy.deepcopy(args[item]) args["request_method"] = method - - if service.cors_enabled: - args["validators"].insert(0, cors_validator) + args["validators"].insert(0, cors_validator) decorated_view = decorate_view(view, dict(args), method, route_args) @@ -357,17 +333,3 @@ def _mungle_view_args(args, predicate_list): else: # otherwise argument value is just a scalar args[kind] = value - - -def register_resource_views(config, resource): - """Register a resource and it's views. - - :param config: - The pyramid configuration object that will be populated. - :param resource: - The resource class containing the definitions - """ - services = resource._services - - for service in services.values(): - config.add_cornice_service(service) diff --git a/kinto/core/cornice/renderer.py b/kinto/core/cornice/renderer.py index 1fd1ffb4c..36a26b3bd 100644 --- a/kinto/core/cornice/renderer.py +++ b/kinto/core/cornice/renderer.py @@ -3,13 +3,6 @@ from pyramid.response import Response -def bytes_adapter(obj, request): - """Convert bytes objects to strings for json error renderer.""" - if isinstance(obj, bytes): - return obj.decode("utf8") - return obj - - class JSONError(exc.HTTPError): def __init__(self, serializer, serializer_kw, errors, status=400): body = {"status": "error", "errors": errors} @@ -18,6 +11,13 @@ def __init__(self, serializer, serializer_kw, errors, status=400): self.content_type = "application/json" +def bytes_adapter(obj, request): + """Convert bytes objects to strings for json error renderer.""" + if isinstance(obj, bytes): + return obj.decode("utf8") + return obj + + class CorniceRenderer(JSON): """We implement JSON serialization by extending Pyramid's default JSON rendering machinery using our own custom Content-Type logic `[1]`_. @@ -34,8 +34,7 @@ class CorniceRenderer(JSON): acceptable = ("application/json", "text/plain") def __init__(self, *args, **kwargs): - """Adds a `bytes` adapter by default.""" - super(CorniceRenderer, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.add_adapter(bytes, bytes_adapter) def render_errors(self, request): diff --git a/kinto/core/cornice/resource.py b/kinto/core/cornice/resource.py deleted file mode 100644 index a80602e0e..000000000 --- a/kinto/core/cornice/resource.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at http://mozilla.org/MPL/2.0/. -import functools -import warnings - -import venusian - -from kinto.core.cornice import Service - - -def resource(depth=2, **kw): - """Class decorator to declare resources. - - All the methods of this class named by the name of HTTP resources - will be used as such. You can also prefix them by ``"collection_"`` and - they will be treated as HTTP methods for the given collection path - (collection_path), if any. - - :param depth: - Which frame should be looked in default 2. - - :param kw: - Keyword arguments configuring the resource. - - Here is an example:: - - @resource(collection_path='/users', path='/users/{id}') - """ - - def wrapper(klass): - return add_resource(klass, depth, **kw) - - return wrapper - - -def add_resource(klass, depth=2, **kw): - """Function to declare resources of a Class. - - All the methods of this class named by the name of HTTP resources - will be used as such. You can also prefix them by ``"collection_"`` and - they will be treated as HTTP methods for the given collection path - (collection_path), if any. - - :param klass: - The class (resource) on which to register the service. - - :param depth: - Which frame should be looked in default 2. - - :param kw: - Keyword arguments configuring the resource. - - - Here is an example: - - .. code-block:: python - - class User(object): - pass - - add_resource(User, collection_path='/users', path='/users/{id}') - - Alternatively if you want to reuse your existing pyramid routes: - - .. code-block:: python - - class User(object): - pass - - add_resource(User, collection_pyramid_route='users', - pyramid_route='user') - - """ - - services = {} - - if ("collection_pyramid_route" in kw or "pyramid_route" in kw) and ( - "collection_path" in kw or "path" in kw - ): - raise ValueError("You use either paths or route names, not both") - - if "collection_path" in kw: - if kw["collection_path"] == kw["path"]: - msg = "Warning: collection_path and path are not distinct." - warnings.warn(msg) - - prefixes = ("", "collection_") - else: - prefixes = ("",) - - if "collection_pyramid_route" in kw: - if kw["collection_pyramid_route"] == kw["pyramid_route"]: - msg = "Warning: collection_pyramid_route and pyramid_route are not distinct." - warnings.warn(msg) - - prefixes = ("", "collection_") - - for prefix in prefixes: - # get clean view arguments - service_args = {} - for k in list(kw): - if k.startswith("collection_"): - if prefix == "collection_": - service_args[k[len(prefix) :]] = kw[k] - elif k not in service_args: - service_args[k] = kw[k] - - # auto-wire klass as its own view factory, unless one - # is explicitly declared. - if "factory" not in kw: - service_args["factory"] = klass - - # create service - service_name = service_args.pop("name", None) or klass.__name__.lower() - service_name = prefix + service_name - service = services[service_name] = Service(name=service_name, depth=depth, **service_args) - # ensure the service comes with the same properties as the wrapped - # resource - functools.update_wrapper(service, klass) - - # initialize views - for verb in ("get", "post", "put", "delete", "options", "patch"): - view_attr = prefix + verb - meth = getattr(klass, view_attr, None) - - if meth is not None: - # if the method has a __views__ arguments, then it had - # been decorated by a @view decorator. get back the name of - # the decorated method so we can register it properly - views = getattr(meth, "__views__", []) - if views: - for view_args in views: - service.add_view(verb, view_attr, klass=klass, **view_args) - else: - service.add_view(verb, view_attr, klass=klass) - - setattr(klass, "_services", services) - - def callback(context, name, ob): - # get the callbacks registered by the inner services - # and call them from here when the @resource classes are being - # scanned by venusian. - for service in services.values(): - config = context.config.with_package(info.module) - config.add_cornice_service(service) - - info = venusian.attach(klass, callback, category="pyramid", depth=depth) - - return klass - - -def view(**kw): - """Method decorator to store view arguments when defining a resource with - the @resource class decorator - - :param kw: - Keyword arguments configuring the view. - """ - - def wrapper(func): - return add_view(func, **kw) - - return wrapper - - -def add_view(func, **kw): - """Method to store view arguments when defining a resource with - the add_resource class method - - :param func: - The func to hook to - - :param kw: - Keyword arguments configuring the view. - - Example: - - .. code-block:: python - - class User(object): - - def __init__(self, request): - self.request = request - - def collection_get(self): - return {'users': _USERS.keys()} - - def get(self): - return _USERS.get(int(self.request.matchdict['id'])) - - add_view(User.get, renderer='json') - add_resource(User, collection_path='/users', path='/users/{id}') - """ - # XXX needed in py2 to set on instancemethod - if hasattr(func, "__func__"): # pragma: no cover - func = func.__func__ - # store view argument to use them later in @resource - views = getattr(func, "__views__", None) - if views is None: - views = [] - setattr(func, "__views__", views) - views.append(kw) - return func diff --git a/kinto/core/cornice/service.py b/kinto/core/cornice/service.py index a8aa322f2..735f01ee2 100644 --- a/kinto/core/cornice/service.py +++ b/kinto/core/cornice/service.py @@ -10,7 +10,6 @@ from kinto.core.cornice.util import func_name, is_string, to_list from kinto.core.cornice.validators import ( - DEFAULT_FILTERS, DEFAULT_VALIDATORS, ) @@ -64,10 +63,6 @@ class Service(object): A list of callables to pass the request into before passing it to the associated view. - :param filters: - A list of callables to pass the response into before returning it to - the client. - :param accept: A list of ``Accept`` header values accepted for this service (or method if overwritten when defining a method). @@ -108,10 +103,6 @@ class Service(object): CORS (Cross Origin Resource Sharing). You can read the CORS specification at http://www.w3.org/TR/cors/ - :param cors_enabled: - To use if you especially want to disable CORS support for a particular - service / method. - :param cors_origins: The list of origins for CORS. You can use wildcards here if needed, e.g. ('list', 'of', '\\*.domain'). @@ -153,10 +144,9 @@ class Service(object): renderer = "cornicejson" default_validators = DEFAULT_VALIDATORS - default_filters = DEFAULT_FILTERS mandatory_arguments = ("renderer",) - list_arguments = ("validators", "filters", "cors_headers", "cors_origins") + list_arguments = ("validators", "cors_headers", "cors_origins") def __repr__(self): return "" % (self.name, self.pyramid_route or self.path) @@ -180,14 +170,13 @@ def __init__( self.description = description self.cors_expose_all_headers = True - self._cors_enabled = None if cors_policy: for key, value in cors_policy.items(): kw.setdefault("cors_" + key, value) for key in self.list_arguments: - # default_{validators,filters} and {filters,validators} don't + # default_validators and validators don't # have to be mutables, so we need to create a new list from them extra = to_list(kw.get(key, [])) kw[key] = [] @@ -264,10 +253,10 @@ def get_arguments(self, conf=None): "error_handler", getattr(self, "error_handler", self.default_error_handler) ) - # exclude some validators or filters + # exclude some validators if "exclude" in conf: for item in to_list(conf.pop("exclude")): - for container in arguments["validators"], arguments["filters"]: + for container in (arguments["validators"],): if item in container: container.remove(item) @@ -457,30 +446,6 @@ def get_contenttypes(self, method, filter_callables=False): """ return self.filter_argumentlist(method, "content_type", filter_callables) - def get_validators(self, method): - """return a list of validators for the given method. - - :param method: the method to get the validators for. - """ - validators = [] - for meth, view, args in self.definitions: - if meth.upper() == method.upper() and "validators" in args: - for validator in args["validators"]: - if validator not in validators: - validators.append(validator) - return validators - - @property - def cors_enabled(self): - if self._cors_enabled is False: - return False - - return bool(self.cors_origins or self._cors_enabled) - - @cors_enabled.setter - def cors_enabled(self, value): - self._cors_enabled = value - def cors_supported_headers_for(self, method=None): """Return an iterable of supported headers for this service. @@ -489,13 +454,12 @@ def cors_supported_headers_for(self, method=None): """ headers = set() for meth, _, args in self.definitions: - if args.get("cors_enabled", True): - exposed_headers = args.get("cors_headers", ()) - if method is not None: - if meth.upper() == method.upper(): - return set(exposed_headers) - else: - headers |= set(exposed_headers) + exposed_headers = args.get("cors_headers", ()) + if method is not None: + if meth.upper() == method.upper(): + return set(exposed_headers) + else: + headers |= set(exposed_headers) return headers @property @@ -503,7 +467,7 @@ def cors_supported_methods(self): """Return an iterable of methods supported by CORS""" methods = [] for meth, _, args in self.definitions: - if args.get("cors_enabled", True) and meth not in methods: + if meth not in methods: methods.append(meth) return methods diff --git a/kinto/core/cornice/util.py b/kinto/core/cornice/util.py index 7a705116f..a32535152 100644 --- a/kinto/core/cornice/util.py +++ b/kinto/core/cornice/util.py @@ -1,7 +1,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -import warnings __all__ = [ @@ -69,18 +68,6 @@ def match_content_type_header(func, context, request): return content_type_matches(request, supported_contenttypes) -def extract_json_data(request): - warnings.warn("Use ``cornice.validators.extract_cstruct()`` instead", DeprecationWarning) - from kinto.core.cornice.validators import extract_cstruct - - return extract_cstruct(request)["body"] - - -def extract_form_urlencoded_data(request): - warnings.warn("Use ``cornice.validators.extract_cstruct()`` instead", DeprecationWarning) - return request.POST - - def content_type_matches(request, content_types): """ Check whether ``request.content_type`` diff --git a/kinto/core/cornice/validators/__init__.py b/kinto/core/cornice/validators/__init__.py index ee2c03ab6..15763e2d4 100755 --- a/kinto/core/cornice/validators/__init__.py +++ b/kinto/core/cornice/validators/__init__.py @@ -5,35 +5,11 @@ from webob.multidict import MultiDict -from kinto.core.cornice.validators._colander import body_validator as colander_body_validator -from kinto.core.cornice.validators._colander import headers_validator as colander_headers_validator -from kinto.core.cornice.validators._colander import path_validator as colander_path_validator -from kinto.core.cornice.validators._colander import ( - querystring_validator as colander_querystring_validator, -) from kinto.core.cornice.validators._colander import validator as colander_validator -from kinto.core.cornice.validators._marshmallow import body_validator as marshmallow_body_validator -from kinto.core.cornice.validators._marshmallow import ( - headers_validator as marshmallow_headers_validator, -) -from kinto.core.cornice.validators._marshmallow import path_validator as marshmallow_path_validator -from kinto.core.cornice.validators._marshmallow import ( - querystring_validator as marshmallow_querystring_validator, -) -from kinto.core.cornice.validators._marshmallow import validator as marshmallow_validator __all__ = [ "colander_validator", - "colander_body_validator", - "colander_headers_validator", - "colander_path_validator", - "colander_querystring_validator", - "marshmallow_validator", - "marshmallow_body_validator", - "marshmallow_headers_validator", - "marshmallow_path_validator", - "marshmallow_querystring_validator", "extract_cstruct", "DEFAULT_VALIDATORS", "DEFAULT_FILTERS", @@ -41,7 +17,6 @@ DEFAULT_VALIDATORS = [] -DEFAULT_FILTERS = [] def extract_cstruct(request): diff --git a/kinto/core/cornice/validators/_colander.py b/kinto/core/cornice/validators/_colander.py index 5e5efa531..e110b7513 100644 --- a/kinto/core/cornice/validators/_colander.py +++ b/kinto/core/cornice/validators/_colander.py @@ -2,91 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -import inspect -import warnings - - -def _generate_colander_validator(location): - """ - Generate a colander validator for data from the given location. - - :param location: The location in the request to find the data to be - validated, such as "body" or "querystring". - :type location: str - :return: Returns a callable that will validate the request at the given - location. - :rtype: callable - """ - - def _validator(request, schema=None, deserializer=None, **kwargs): - """ - Validate the location against the schema defined on the service. - - The content of the location is deserialized, validated and stored in - the ``request.validated`` attribute. - - .. note:: - - If no schema is defined, this validator does nothing. - Schema should be of type :class:`~colander:colander.MappingSchema`. - - :param request: Current request - :type request: :class:`~pyramid:pyramid.request.Request` - - :param schema: The Colander schema - :param deserializer: Optional deserializer, defaults to - :func:`cornice.validators.extract_cstruct` - """ - import colander - - if schema is None: - return - - schema_instance = _ensure_instantiated(schema) - - if not isinstance(schema_instance, colander.MappingSchema): - raise TypeError("Schema should inherit from colander.MappingSchema.") - - class RequestSchemaMeta(colander._SchemaMeta): - """ - A metaclass that will inject a location class attribute into - RequestSchema. - """ - - def __new__(cls, name, bases, class_attrs): - """ - Instantiate the RequestSchema class. - - :param name: The name of the class we are instantiating. Will - be "RequestSchema". - :type name: str - :param bases: The class's superclasses. - :type bases: tuple - :param dct: The class's class attributes. - :type dct: dict - """ - class_attrs[location] = schema_instance - return type(name, bases, class_attrs) - - class RequestSchema(colander.MappingSchema, metaclass=RequestSchemaMeta): # noqa - """A schema to validate the request's location attributes.""" - - pass - - validator(request, RequestSchema(), deserializer, **kwargs) - validated_location = request.validated.get(location, {}) - request.validated.update(validated_location) - if location not in validated_location: - request.validated.pop(location, None) - - return _validator - - -body_validator = _generate_colander_validator("body") -headers_validator = _generate_colander_validator("headers") -path_validator = _generate_colander_validator("path") -querystring_validator = _generate_colander_validator("querystring") - def validator(request, schema=None, deserializer=None, **kwargs): """ @@ -117,7 +32,6 @@ def validator(request, schema=None, deserializer=None, **kwargs): if schema is None: return - schema = _ensure_instantiated(schema) cstruct = deserializer(request) try: deserialized = schema.deserialize(cstruct) @@ -129,14 +43,3 @@ def validator(request, schema=None, deserializer=None, **kwargs): request.errors.add(location, field, msg) else: request.validated.update(deserialized) - - -def _ensure_instantiated(schema): - if inspect.isclass(schema): - warnings.warn( - "Setting schema to a class is deprecated. Set schema to an instance instead.", - DeprecationWarning, - stacklevel=2, - ) - schema = schema() - return schema diff --git a/kinto/core/cornice/validators/_marshmallow.py b/kinto/core/cornice/validators/_marshmallow.py deleted file mode 100644 index 8afd4de52..000000000 --- a/kinto/core/cornice/validators/_marshmallow.py +++ /dev/null @@ -1,182 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at http://mozilla.org/MPL/2.0/. - -import inspect - - -def _generate_marshmallow_validator(location): - """ - Generate a marshmallow validator for data from the given location. - - :param location: The location in the request to find the data to be - validated, such as "body" or "querystring". - :type location: str - :return: Returns a callable that will validate the request at the given - location. - :rtype: callable - """ - - def _validator(request, schema=None, deserializer=None, **kwargs): - """ - Validate the location against the schema defined on the service. - - The content of the location is deserialized, validated and stored in - the ``request.validated`` attribute. - - Keyword arguments to be included when initialising the marshmallow - schema can be passed as a dict in ``kwargs['schema_kwargs']`` variable. - - .. note:: - - If no schema is defined, this validator does nothing. - - :param request: Current request - :type request: :class:`~pyramid:pyramid.request.Request` - - :param schema: The marshmallow schema - :param deserializer: Optional deserializer, defaults to - :func:`cornice.validators.extract_cstruct` - """ - import marshmallow - import marshmallow.schema - from marshmallow.utils import EXCLUDE - - if schema is None: - return - - # see if the user wants to set any keyword arguments for their schema - schema_kwargs = kwargs.get("schema_kwargs", {}) - schema = _instantiate_schema(schema, **schema_kwargs) - - class ValidatedField(marshmallow.fields.Field): - def _deserialize(self, value, attr, data, **kwargs): - schema.context.setdefault("request", request) - deserialized = schema.load(value) - return deserialized - - class Meta(object): - strict = True - ordered = True - unknown = EXCLUDE - - class RequestSchemaMeta(marshmallow.schema.SchemaMeta): - """ - A metaclass that will inject a location class attribute into - RequestSchema. - """ - - def __new__(cls, name, bases, class_attrs): - """ - Instantiate the RequestSchema class. - - :param name: The name of the class we are instantiating. Will - be "RequestSchema". - :type name: str - :param bases: The class's superclasses. - :type bases: tuple - :param dct: The class's class attributes. - :type dct: dict - """ - - class_attrs[location] = ValidatedField( - required=True, metadata={"load_from": location} - ) - class_attrs["Meta"] = Meta - return type(name, bases, class_attrs) - - class RequestSchema(marshmallow.Schema, metaclass=RequestSchemaMeta): # noqa - """A schema to validate the request's location attributes.""" - - pass - - validator(request, RequestSchema, deserializer, **kwargs) - request.validated = request.validated.get(location, {}) - - return _validator - - -body_validator = _generate_marshmallow_validator("body") -headers_validator = _generate_marshmallow_validator("header") -path_validator = _generate_marshmallow_validator("path") -querystring_validator = _generate_marshmallow_validator("querystring") - - -def _message_normalizer(exc, no_field_name="_schema"): - """ - Normally `normalize_messages` will exist on `ValidationError` but pre 2.10 - versions don't have it - :param exc: - :param no_field_name: - :return: - """ - if isinstance(exc.messages, dict): - return exc.messages - field_names = exc.kwargs.get("field_names", []) - if len(field_names) == 0: - return {no_field_name: exc.messages} - return dict((name, exc.messages) for name in field_names) - - -def validator(request, schema=None, deserializer=None, **kwargs): - """ - Validate the full request against the schema defined on the service. - - Each attribute of the request is deserialized, validated and stored in the - ``request.validated`` attribute - (eg. body in ``request.validated['body']``). - - .. note:: - - If no schema is defined, this validator does nothing. - - :param request: Current request - :type request: :class:`~pyramid:pyramid.request.Request` - - :param schema: The marshmallow schema - :param deserializer: Optional deserializer, defaults to - :func:`cornice.validators.extract_cstruct` - """ - import marshmallow - - from kinto.core.cornice.validators import extract_cstruct - - if deserializer is None: - deserializer = extract_cstruct - - if schema is None: - return - - schema = _instantiate_schema(schema) - schema.context.setdefault("request", request) - - cstruct = deserializer(request) - try: - deserialized = schema.load(cstruct) - except marshmallow.ValidationError as err: - # translate = request.localizer.translate - normalized_errors = _message_normalizer(err) - for location, details in normalized_errors.items(): - location = location if location != "_schema" else "" - if hasattr(details, "items"): - for subfield, msg in details.items(): - request.errors.add(location, subfield, msg) - else: - request.errors.add(location, location, details) - else: - request.validated.update(deserialized) - - -def _instantiate_schema(schema, **kwargs): - """ - Returns an object of the given marshmallow schema. - - :param schema: The marshmallow schema class with which the request should - be validated - :param kwargs: The keyword arguments that will be provided to the - marshmallow schema's constructor - :return: The object of the marshmallow schema - """ - if not inspect.isclass(schema): - raise ValueError("You need to pass Marshmallow class instead of schema instance") - return schema(**kwargs) diff --git a/kinto/core/cornice_swagger/__init__.py b/kinto/core/cornice_swagger/__init__.py index 6f7a9f729..c5858cebc 100644 --- a/kinto/core/cornice_swagger/__init__.py +++ b/kinto/core/cornice_swagger/__init__.py @@ -1,5 +1,3 @@ -from pyramid.security import NO_PERMISSION_REQUIRED - from kinto.core.cornice_swagger.swagger import CorniceSwagger @@ -30,63 +28,3 @@ def includeme(config): config.add_view_predicate("tags", CorniceSwaggerPredicate) config.add_view_predicate("operation_id", CorniceSwaggerPredicate) config.add_view_predicate("api_security", CorniceSwaggerPredicate) - config.add_directive("cornice_enable_openapi_view", cornice_enable_openapi_view) - config.add_directive("cornice_enable_openapi_explorer", cornice_enable_openapi_explorer) - - -def cornice_enable_openapi_view( - config, - api_path="/api-explorer/swagger.json", - permission=NO_PERMISSION_REQUIRED, - route_factory=None, - **kwargs, -): - """ - :param config: - Pyramid configurator object - :param api_path: - where to expose swagger JSON definition view - :param permission: - pyramid permission for those views - :param route_factory: - factory for context object for those routes - :param kwargs: - kwargs that will be passed to CorniceSwagger's `generate()` - - This registers and configures the view that serves api definitions - """ - config.registry.settings["cornice_swagger.spec_kwargs"] = kwargs - config.add_route("cornice_swagger.open_api_path", api_path, factory=route_factory) - config.add_view( - "cornice_swagger.views.open_api_json_view", - renderer="json", - permission=permission, - route_name="cornice_swagger.open_api_path", - ) - - -def cornice_enable_openapi_explorer( - config, - api_explorer_path="/api-explorer", - permission=NO_PERMISSION_REQUIRED, - route_factory=None, - **kwargs, -): - """ - :param config: - Pyramid configurator object - :param api_explorer_path: - where to expose Swagger UI interface view - :param permission: - pyramid permission for those views - :param route_factory: - factory for context object for those routes - - This registers and configures the view that serves api explorer - """ - config.add_route("cornice_swagger.api_explorer_path", api_explorer_path, factory=route_factory) - config.add_view( - "cornice_swagger.views.swagger_ui_template_view", - permission=permission, - route_name="cornice_swagger.api_explorer_path", - ) diff --git a/kinto/core/cornice_swagger/swagger.py b/kinto/core/cornice_swagger/swagger.py index c5cf53252..872e5ca64 100644 --- a/kinto/core/cornice_swagger/swagger.py +++ b/kinto/core/cornice_swagger/swagger.py @@ -1,6 +1,5 @@ """Cornice Swagger 2.0 documentor""" -import inspect import warnings from collections import OrderedDict @@ -13,7 +12,7 @@ ParameterConversionDispatcher as ParameterConverter, ) from kinto.core.cornice_swagger.converters import TypeConversionDispatcher as TypeConverter -from kinto.core.cornice_swagger.util import body_schema_transformer, merge_dicts, trim +from kinto.core.cornice_swagger.util import merge_dicts, trim class CorniceSwaggerException(Exception): @@ -299,10 +298,6 @@ class CorniceSwagger(object): """Default :class:`cornice_swagger.swagger.ResponseHandler` class to use when handling OpenAPI responses from kinto.core.cornice_swagger defined responses.""" - schema_transformers = [body_schema_transformer] - """List of request schema transformers that should be applied to a request - schema to make it comply with a cornice default request schema.""" - type_converter = TypeConverter """Default :class:`cornice_swagger.converters.schema.TypeConversionDispatcher` class used for converting colander schema Types to Swagger Types.""" @@ -372,7 +367,7 @@ def __init__( """ :param services: List of cornice services to document. You may use - cornice.service.get_services() to get it. + kinto.core.cornice.service.get_services() to get it. :param def_ref_depth: How depth swagger object schemas should be split into swaggger definitions with JSON pointers. Default (0) is no split. @@ -658,15 +653,9 @@ def _extract_operation_from_view(self, view, args): op["consumes"] = consumes # Get parameters from view schema - is_colander = self._is_colander_schema(args) - if is_colander: - schema = self._extract_transform_colander_schema(args) - parameters = self.parameters.from_schema(schema) - else: - # Bail out for now - parameters = None - if parameters: - op["parameters"] = parameters + schema = args.get("schema", colander.MappingSchema()) + parameters = self.parameters.from_schema(schema) + op["parameters"] = parameters # Get summary from docstring if isinstance(view, str): @@ -697,29 +686,3 @@ def _extract_operation_from_view(self, view, args): op["security"] = args["api_security"] return op - - def _is_colander_schema(self, args): - schema = args.get("schema") - return isinstance(schema, colander.Schema) or ( - inspect.isclass(schema) and issubclass(schema, colander.MappingSchema) - ) - - def _extract_transform_colander_schema(self, args): - """ - Extract schema from view args and transform it using - the pipeline of schema transformers - - :param args: - Arguments from the view decorator. - - :rtype: colander.MappingSchema() - :returns: View schema cloned and transformed - """ - - schema = args.get("schema", colander.MappingSchema()) - if not isinstance(schema, colander.Schema): - schema = schema() - schema = schema.clone() - for transformer in self.schema_transformers: - schema = transformer(schema, args) - return schema diff --git a/kinto/core/cornice_swagger/templates/index.html b/kinto/core/cornice_swagger/templates/index.html deleted file mode 100644 index 6d5c53076..000000000 --- a/kinto/core/cornice_swagger/templates/index.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - Swagger UI - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -${swagger_ui_script} - - - diff --git a/kinto/core/cornice_swagger/templates/index_script_template.html b/kinto/core/cornice_swagger/templates/index_script_template.html deleted file mode 100644 index 1bf6f4f1b..000000000 --- a/kinto/core/cornice_swagger/templates/index_script_template.html +++ /dev/null @@ -1,21 +0,0 @@ - diff --git a/kinto/core/cornice_swagger/util.py b/kinto/core/cornice_swagger/util.py index 9696d1a89..47fd8300f 100644 --- a/kinto/core/cornice_swagger/util.py +++ b/kinto/core/cornice_swagger/util.py @@ -1,8 +1,3 @@ -import colander - -from kinto.core.cornice.validators import colander_body_validator - - def trim(docstring): """ Remove the tabs to spaces, and remove the extra spaces / tabs that are in @@ -20,15 +15,6 @@ def trim(docstring): return res -def body_schema_transformer(schema, args): - validators = args.get("validators", []) - if colander_body_validator in validators: - body_schema = schema - schema = colander.MappingSchema() - schema["body"] = body_schema - return schema - - def merge_dicts(base, changes): """Merge b into a recursively, without overwriting values. diff --git a/kinto/core/cornice_swagger/views.py b/kinto/core/cornice_swagger/views.py deleted file mode 100644 index 93d75cf05..000000000 --- a/kinto/core/cornice_swagger/views.py +++ /dev/null @@ -1,78 +0,0 @@ -import importlib -from string import Template - -import cornice -import cornice_swagger -import pkg_resources -from pyramid.response import Response - - -# hardcode for now since that will work for vast majority of users -# maybe later add minified resources for behind firewall support? -ui_css_url = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.23.11/swagger-ui.css" -ui_js_bundle_url = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.23.11/swagger-ui-bundle.js" -ui_js_standalone_url = ( - "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.23.11/swagger-ui-standalone-preset.js" -) - - -def swagger_ui_template_view(request): - """ - Serves Swagger UI page, default Swagger UI config is used but you can - override the callable that generates the `