diff --git a/connexion/__init__.py b/connexion/__init__.py index db5741d01..7c74ac210 100755 --- a/connexion/__init__.py +++ b/connexion/__init__.py @@ -7,24 +7,16 @@ specified. """ -import sys - import werkzeug.exceptions as exceptions # NOQA from .apis import AbstractAPI # NOQA from .apps import AbstractApp # NOQA from .decorators.produces import NoContent # NOQA from .exceptions import ProblemException # NOQA -# add operation for backwards compatibility -from .operations import compat from .problem import problem # NOQA from .resolver import Resolution, Resolver, RestyResolver # NOQA from .utils import not_installed_error # NOQA -full_name = f'{__package__}.operation' -sys.modules[full_name] = sys.modules[compat.__name__] - - try: from flask import request # NOQA diff --git a/connexion/apis/abstract.py b/connexion/apis/abstract.py index acc365106..28cc9aaca 100644 --- a/connexion/apis/abstract.py +++ b/connexion/apis/abstract.py @@ -140,22 +140,13 @@ def __init__( self.debug = debug self.resolver_error_handler = resolver_error_handler - logger.debug('Security Definitions: %s', self.specification.security_definitions) - self.resolver = resolver or Resolver() logger.debug('pass_context_arg_name: %s', pass_context_arg_name) self.pass_context_arg_name = pass_context_arg_name - self.security_handler_factory = self.make_security_handler_factory(pass_context_arg_name) - self.add_paths() - @staticmethod - @abc.abstractmethod - def make_security_handler_factory(pass_context_arg_name): - """ Create SecurityHandlerFactory to create all security check handlers """ - def add_paths(self, paths: t.Optional[dict] = None) -> None: """ Adds the paths defined in the specification as endpoints @@ -196,8 +187,6 @@ def _add_resolver_error_handler(self, method: str, path: str, err: ResolverError """ operation = self.resolver_error_handler( err, - security=self.specification.security, - security_definitions=self.specification.security_definitions ) self._add_operation_internal(method, path, operation) @@ -221,13 +210,11 @@ class AbstractAPI(AbstractMinimalAPI, metaclass=AbstractAPIMeta): def __init__(self, specification, base_path=None, arguments=None, validate_responses=False, strict_validation=False, resolver=None, - auth_all_paths=False, debug=False, resolver_error_handler=None, - validator_map=None, pythonic_params=False, pass_context_arg_name=None, options=None, - ): + debug=False, resolver_error_handler=None, validator_map=None, + pythonic_params=False, pass_context_arg_name=None, options=None, **kwargs): """ :type validate_responses: bool :type strict_validation: bool - :type auth_all_paths: bool :param validator_map: Custom validators for the types "parameter", "body" and "response". :type validator_map: dict :type resolver_error_handler: callable | None @@ -247,22 +234,9 @@ def __init__(self, specification, base_path=None, arguments=None, self.pythonic_params = pythonic_params super().__init__(specification, base_path=base_path, arguments=arguments, - resolver=resolver, auth_all_paths=auth_all_paths, - resolver_error_handler=resolver_error_handler, + resolver=resolver, resolver_error_handler=resolver_error_handler, debug=debug, pass_context_arg_name=pass_context_arg_name, options=options) - if auth_all_paths: - self.add_auth_on_not_found( - self.specification.security, - self.specification.security_definitions - ) - - @abc.abstractmethod - def add_auth_on_not_found(self, security, security_definitions): - """ - Adds a 404 error handler to authenticate and only expose the 404 status if the security validation pass. - """ - def add_operation(self, path, method): """ Adds one operation to the api. diff --git a/connexion/apis/flask_api.py b/connexion/apis/flask_api.py index eee16dcdf..da15bbbcd 100644 --- a/connexion/apis/flask_api.py +++ b/connexion/apis/flask_api.py @@ -8,15 +8,12 @@ from typing import Any import flask -import werkzeug.exceptions from werkzeug.local import LocalProxy from connexion.apis import flask_utils from connexion.apis.abstract import AbstractAPI -from connexion.handlers import AuthErrorHandler from connexion.jsonifier import Jsonifier from connexion.lifecycle import ConnexionRequest, ConnexionResponse -from connexion.security import FlaskSecurityHandlerFactory from connexion.utils import is_json_mimetype logger = logging.getLogger('connexion.apis.flask_api') @@ -24,11 +21,6 @@ class FlaskApi(AbstractAPI): - @staticmethod - def make_security_handler_factory(pass_context_arg_name): - """ Create default SecurityHandlerFactory to create all security check handlers """ - return FlaskSecurityHandlerFactory(pass_context_arg_name) - def _set_base_path(self, base_path): super()._set_base_path(base_path) self._set_blueprint() @@ -39,16 +31,6 @@ def _set_blueprint(self): self.blueprint = flask.Blueprint(endpoint, __name__, url_prefix=self.base_path, template_folder=str(self.options.openapi_console_ui_from_dir)) - def add_auth_on_not_found(self, security, security_definitions): - """ - Adds a 404 error handler to authenticate and only expose the 404 status if the security validation pass. - """ - logger.debug('Adding path not found authentication') - not_found_error = AuthErrorHandler(self, werkzeug.exceptions.NotFound(), security=security, - security_definitions=security_definitions) - endpoint_name = f"{self.blueprint.name}_not_found" - self.blueprint.add_url_rule('/', endpoint_name, not_found_error.function) - def _add_operation_internal(self, method, path, operation): operation_id = operation.operation_id logger.debug('... Adding %s -> %s', method.upper(), operation_id, @@ -156,9 +138,10 @@ def get_request(cls, *args, **params): :rtype: ConnexionRequest """ - context_dict = {} - setattr(flask._request_ctx_stack.top, 'connexion_context', context_dict) flask_request = flask.request + scope = flask_request.environ['asgi.scope'] + context_dict = scope.get('extensions', {}).get('connexion_context', {}) + setattr(flask._request_ctx_stack.top, 'connexion_context', context_dict) request = ConnexionRequest( flask_request.url, flask_request.method, diff --git a/connexion/apps/abstract.py b/connexion/apps/abstract.py index d6fe6d5bb..a9110ae4c 100644 --- a/connexion/apps/abstract.py +++ b/connexion/apps/abstract.py @@ -193,7 +193,7 @@ def add_api(self, specification, base_path=None, arguments=None, def _resolver_error_handler(self, *args, **kwargs): from connexion.handlers import ResolverErrorHandler - return ResolverErrorHandler(self.api_cls, self.resolver_error, *args, **kwargs) + return ResolverErrorHandler(self.resolver_error, *args, **kwargs) def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """ diff --git a/connexion/exceptions.py b/connexion/exceptions.py index e1311f3e1..a112dc1dd 100644 --- a/connexion/exceptions.py +++ b/connexion/exceptions.py @@ -5,7 +5,7 @@ import warnings from jsonschema.exceptions import ValidationError -from werkzeug.exceptions import Forbidden, Unauthorized +from starlette.exceptions import HTTPException from .problem import problem @@ -61,6 +61,10 @@ class InvalidSpecification(ConnexionException, ValidationError): pass +class MissingMiddleware(ConnexionException): + pass + + class NonConformingResponse(ProblemException): def __init__(self, reason='Unknown Reason', message=None): """ @@ -123,6 +127,19 @@ def __init__(self, message, reason="Response headers do not conform to specifica super().__init__(reason=reason, message=message) +class Unauthorized(HTTPException): + + description = ( + "The server could not verify that you are authorized to access" + " the URL requested. You either supplied the wrong credentials" + " (e.g. a bad password), or your browser doesn't understand" + " how to supply the credentials required." + ) + + def __init__(self, detail: str = description, **kwargs): + super().__init__(401, detail=detail, **kwargs) + + class OAuthProblem(Unauthorized): pass @@ -133,6 +150,18 @@ def __init__(self, token_response, **kwargs): super().__init__(**kwargs) +class Forbidden(HTTPException): + + description = ( + "You don't have the permission to access the requested" + " resource. It is either read-protected or not readable by the" + " server." + ) + + def __init__(self, detail: str = description, **kwargs): + super().__init__(403, detail=detail, **kwargs) + + class OAuthScopeProblem(Forbidden): def __init__(self, token_scopes, required_scopes, **kwargs): self.required_scopes = required_scopes diff --git a/connexion/handlers.py b/connexion/handlers.py index f8d8d9966..c5198d2a4 100644 --- a/connexion/handlers.py +++ b/connexion/handlers.py @@ -4,67 +4,21 @@ import logging -from .exceptions import AuthenticationProblem, ResolverProblem -from .operations.secure import SecureOperation +from .exceptions import ResolverProblem logger = logging.getLogger('connexion.handlers') RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS = 6 -class AuthErrorHandler(SecureOperation): - """ - Wraps an error with authentication. - """ - - def __init__(self, api, exception, security, security_definitions): - """ - This class uses the exception instance to produce the proper response problem in case the - request is authenticated. - - :param exception: the exception to be wrapped with authentication - :type exception: werkzeug.exceptions.HTTPException - :param security: list of security rules the application uses by default - :type security: list - :param security_definitions: `Security Definitions Object - `_ - :type security_definitions: dict - """ - self.exception = exception - super().__init__(api, security, security_definitions) - - @property - def function(self): - """ - Configured error auth handler. - """ - security_decorator = self.security_decorator - logger.debug('... Adding security decorator (%r)', security_decorator, extra=vars(self)) - function = self.handle - function = security_decorator(function) - function = self._request_response_decorator(function) - return function - - def handle(self, *args, **kwargs): - """ - Actual handler for the execution after authentication. - """ - raise AuthenticationProblem( - title=self.exception.name, - detail=self.exception.description, - status=self.exception.code - ) - - -class ResolverErrorHandler(SecureOperation): +class ResolverErrorHandler: """ Handler for responding to ResolverError. """ - def __init__(self, api, status_code, exception, security, security_definitions): + def __init__(self, status_code, exception): self.status_code = status_code self.exception = exception - super().__init__(api, security, security_definitions) @property def function(self): diff --git a/connexion/lifecycle.py b/connexion/lifecycle.py index 231d9d650..8cff5aa64 100644 --- a/connexion/lifecycle.py +++ b/connexion/lifecycle.py @@ -59,6 +59,18 @@ def __init__(self, class MiddlewareRequest(StarletteRequest): """Wraps starlette Request so it can easily be extended.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._context = None + + @property + def context(self): + if self._context is None: + extensions = self.scope.setdefault('extensions', {}) + self._context = extensions.setdefault('connexion_context', {}) + + return self._context + class MiddlewareResponse(StarletteStreamingResponse): """Wraps starlette StreamingResponse so it can easily be extended.""" diff --git a/connexion/middleware/exceptions.py b/connexion/middleware/exceptions.py index 3e075781f..35122837a 100644 --- a/connexion/middleware/exceptions.py +++ b/connexion/middleware/exceptions.py @@ -6,13 +6,38 @@ from starlette.requests import Request from starlette.responses import Response -from connexion.exceptions import problem +from connexion.exceptions import ProblemException, problem class ExceptionMiddleware(StarletteExceptionMiddleware): """Subclass of starlette ExceptionMiddleware to change handling of HTTP exceptions to existing connexion behavior.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_exception_handler(ProblemException, self.problem_handler) + + def problem_handler(self, _, exception: ProblemException): + """ + :type exception: Exception + """ + connexion_response = problem( + status=exception.status, + title=exception.title, + detail=exception.detail, + type=exception.type, + instance=exception.instance, + headers=exception.headers, + ext=exception.ext + ) + + return Response( + content=json.dumps(connexion_response.body), + status_code=connexion_response.status_code, + media_type=connexion_response.mimetype, + headers=connexion_response.headers + ) + def http_exception(self, request: Request, exc: HTTPException) -> Response: try: headers = exc.headers diff --git a/connexion/middleware/main.py b/connexion/middleware/main.py index 863abf4bb..817ecc7cd 100644 --- a/connexion/middleware/main.py +++ b/connexion/middleware/main.py @@ -6,6 +6,7 @@ from connexion.middleware.abstract import AppMiddleware from connexion.middleware.exceptions import ExceptionMiddleware from connexion.middleware.routing import RoutingMiddleware +from connexion.middleware.security import SecurityMiddleware from connexion.middleware.swagger_ui import SwaggerUIMiddleware @@ -15,6 +16,7 @@ class ConnexionMiddleware: ExceptionMiddleware, SwaggerUIMiddleware, RoutingMiddleware, + SecurityMiddleware, ] def __init__( diff --git a/connexion/middleware/routing.py b/connexion/middleware/routing.py index 08e528c18..9f5444dcd 100644 --- a/connexion/middleware/routing.py +++ b/connexion/middleware/routing.py @@ -13,7 +13,7 @@ from connexion.operations import AbstractOperation, make_operation from connexion.resolver import Resolver -CONNEXION_CONTEXT = 'connexion.context' +ROUTING_CONTEXT = 'connexion_routing' _scope_receive_send: ContextVar[tuple] = ContextVar('SCOPE_RECEIVE_SEND') @@ -84,8 +84,8 @@ async def default_fn(self, scope: Scope, receive: Receive, send: Send) -> None: api_base_path = scope.get('root_path', '')[len(original_scope.get('root_path', '')):] extensions = original_scope.setdefault('extensions', {}) - connexion_context = extensions.setdefault(CONNEXION_CONTEXT, {}) - connexion_context.update({ + connexion_routing = extensions.setdefault(ROUTING_CONTEXT, {}) + connexion_routing.update({ 'api_base_path': api_base_path }) await self.app(original_scope, receive, send) @@ -102,8 +102,8 @@ async def call_next( api_base_path = request.scope.get('root_path', '')[len(scope.get('root_path', '')):] extensions = scope.setdefault('extensions', {}) - connexion_context = extensions.setdefault(CONNEXION_CONTEXT, {}) - connexion_context.update({ + connexion_routing = extensions.setdefault(ROUTING_CONTEXT, {}) + connexion_routing.update({ 'api_base_path': api_base_path, 'operation_id': operation.operation_id }) @@ -163,8 +163,3 @@ def patch_operation_function(): def _add_operation_internal(self, method: str, path: str, operation: AbstractOperation) -> None: self.router.add_route(path, operation.function, methods=[method]) - - @staticmethod - def make_security_handler_factory(pass_context_arg_name): - """ Create default SecurityHandlerFactory to create all security check handlers """ - pass diff --git a/connexion/middleware/security.py b/connexion/middleware/security.py new file mode 100644 index 000000000..23d8300df --- /dev/null +++ b/connexion/middleware/security.py @@ -0,0 +1,238 @@ +import logging +import pathlib +import typing as t +from collections import defaultdict + +from starlette.types import ASGIApp, Receive, Scope, Send + +from connexion.apis.abstract import AbstractSpecAPI +from connexion.exceptions import MissingMiddleware +from connexion.http_facts import METHODS +from connexion.lifecycle import MiddlewareRequest +from connexion.middleware import AppMiddleware +from connexion.middleware.routing import ROUTING_CONTEXT +from connexion.security import SecurityHandlerFactory + +logger = logging.getLogger("connexion.middleware.security") + + +class SecurityMiddleware(AppMiddleware): + """Middleware to check if operation is accessible on scope.""" + + def __init__(self, app: ASGIApp) -> None: + self.app = app + self.apis: t.Dict[str, SecurityAPI] = {} + + def add_api(self, specification: t.Union[pathlib.Path, str, dict], **kwargs) -> None: + api = SecurityAPI(specification, **kwargs) + self.apis[api.base_path] = api + + async def __call__(self, scope: Scope, receive: Receive, send: Send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + try: + connexion_context = scope['extensions'][ROUTING_CONTEXT] + except KeyError: + raise MissingMiddleware('Could not find routing information in scope. Please make sure ' + 'you have a routing middleware registered upstream. ') + + api_base_path = connexion_context.get('api_base_path') + if api_base_path: + api = self.apis[api_base_path] + operation_id = connexion_context.get('operation_id') + try: + operation = api.operations[operation_id] + except KeyError as e: + if operation_id is None: + logger.debug('Skipping security check for operation without id. Enable ' + '`auth_all_paths` to check security for unknown operations.') + else: + raise MissingSecurityOperation('Encountered unknown operation_id.') from e + + else: + request = MiddlewareRequest(scope) + await operation(request) + + await self.app(scope, receive, send) + + +class SecurityAPI(AbstractSpecAPI): + + def __init__( + self, + specification: t.Union[pathlib.Path, str, dict], + auth_all_paths: bool = False, + *args, + **kwargs + ): + super().__init__(specification, *args, **kwargs) + self.security_handler_factory = SecurityHandlerFactory('context') + self.app_security = self.specification.security + self.security_schemes = self.specification.security_definitions + + if auth_all_paths: + self.add_auth_on_not_found() + else: + self.operations: t.Dict[str, SecurityOperation] = {} + + self.add_paths() + + def add_auth_on_not_found(self): + """Register a default SecurityOperation for routes that are not found.""" + default_operation = self.make_operation() + self.operations = defaultdict(lambda: default_operation) + + def add_paths(self): + paths = self.specification.get('paths', {}) + for path, methods in paths.items(): + for method, operation in methods.items(): + if method not in METHODS: + continue + operation_id = operation.get('operationId') + if operation_id: + self.operations[operation_id] = self.make_operation(operation) + + def make_operation(self, operation_spec: dict = None): + security = self.app_security + if operation_spec: + security = operation_spec.get('security', self.app_security) + + return SecurityOperation( + self.security_handler_factory, + security=security, + security_schemes=self.specification.security_definitions + ) + + +class SecurityOperation: + + def __init__( + self, + security_handler_factory: SecurityHandlerFactory, + security: list, + security_schemes: dict + ): + self.security_handler_factory = security_handler_factory + self.security = security + self.security_schemes = security_schemes + self.verification_fn = self._get_verification_fn() + + def _get_verification_fn(self): + logger.debug('... Security: %s', self.security, extra=vars(self)) + if not self.security: + return self.security_handler_factory.security_passthrough + + auth_funcs = [] + for security_req in self.security: + if not security_req: + auth_funcs.append(self.security_handler_factory.verify_none()) + continue + + sec_req_funcs = {} + oauth = False + for scheme_name, required_scopes in security_req.items(): + security_scheme = self.security_schemes[scheme_name] + + if security_scheme['type'] == 'oauth2': + if oauth: + logger.warning( + "... multiple OAuth2 security schemes in AND fashion not supported", + extra=vars(self)) + break + oauth = True + token_info_func = self.security_handler_factory.get_tokeninfo_func( + security_scheme) + scope_validate_func = self.security_handler_factory.get_scope_validate_func( + security_scheme) + if not token_info_func: + logger.warning("... x-tokenInfoFunc missing", extra=vars(self)) + break + + sec_req_funcs[scheme_name] = self.security_handler_factory.verify_oauth( + token_info_func, scope_validate_func, required_scopes) + + # Swagger 2.0 + elif security_scheme['type'] == 'basic': + basic_info_func = self.security_handler_factory.get_basicinfo_func( + security_scheme) + if not basic_info_func: + logger.warning("... x-basicInfoFunc missing", extra=vars(self)) + break + + sec_req_funcs[scheme_name] = self.security_handler_factory.verify_basic( + basic_info_func) + + # OpenAPI 3.0.0 + elif security_scheme['type'] == 'http': + scheme = security_scheme['scheme'].lower() + if scheme == 'basic': + basic_info_func = self.security_handler_factory.get_basicinfo_func( + security_scheme) + if not basic_info_func: + logger.warning("... x-basicInfoFunc missing", extra=vars(self)) + break + + sec_req_funcs[ + scheme_name] = self.security_handler_factory.verify_basic( + basic_info_func) + elif scheme == 'bearer': + bearer_info_func = self.security_handler_factory.get_bearerinfo_func( + security_scheme) + if not bearer_info_func: + logger.warning("... x-bearerInfoFunc missing", extra=vars(self)) + break + sec_req_funcs[ + scheme_name] = self.security_handler_factory.verify_bearer( + bearer_info_func) + else: + logger.warning("... Unsupported http authorization scheme %s" % scheme, + extra=vars(self)) + break + + elif security_scheme['type'] == 'apiKey': + scheme = security_scheme.get('x-authentication-scheme', '').lower() + if scheme == 'bearer': + bearer_info_func = self.security_handler_factory.get_bearerinfo_func( + security_scheme) + if not bearer_info_func: + logger.warning("... x-bearerInfoFunc missing", extra=vars(self)) + break + sec_req_funcs[ + scheme_name] = self.security_handler_factory.verify_bearer( + bearer_info_func) + else: + apikey_info_func = self.security_handler_factory.get_apikeyinfo_func( + security_scheme) + if not apikey_info_func: + logger.warning("... x-apikeyInfoFunc missing", extra=vars(self)) + break + + sec_req_funcs[ + scheme_name] = self.security_handler_factory.verify_api_key( + apikey_info_func, security_scheme['in'], security_scheme['name'] + ) + + else: + logger.warning( + "... Unsupported security scheme type %s" % security_scheme['type'], + extra=vars(self)) + break + else: + # No break encountered: no missing funcs + if len(sec_req_funcs) == 1: + (func,) = sec_req_funcs.values() + auth_funcs.append(func) + else: + auth_funcs.append( + self.security_handler_factory.verify_multiple_schemes(sec_req_funcs)) + + return self.security_handler_factory.verify_security(auth_funcs) + + async def __call__(self, request: MiddlewareRequest): + await self.verification_fn(request) + + +class MissingSecurityOperation(Exception): + pass diff --git a/connexion/operations/__init__.py b/connexion/operations/__init__.py index fd380debd..233fd2046 100644 --- a/connexion/operations/__init__.py +++ b/connexion/operations/__init__.py @@ -8,7 +8,6 @@ from .abstract import AbstractOperation # noqa from .openapi import OpenAPIOperation # noqa -from .secure import SecureOperation # noqa from .swagger2 import Swagger2Operation # noqa diff --git a/connexion/operations/abstract.py b/connexion/operations/abstract.py index bf76529cc..1a5abc9a4 100644 --- a/connexion/operations/abstract.py +++ b/connexion/operations/abstract.py @@ -6,8 +6,7 @@ import abc import logging -from connexion.operations.secure import SecureOperation - +from ..decorators.decorator import RequestResponseDecorator from ..decorators.metrics import UWSGIMetricsCollector from ..decorators.parameter import parameter_to_arg from ..decorators.produces import BaseSerializer, Produces @@ -26,7 +25,7 @@ } -class AbstractOperation(SecureOperation, metaclass=abc.ABCMeta): +class AbstractOperation(metaclass=abc.ABCMeta): """ An API routes requests to an Operation by a (path, method) pair. @@ -45,7 +44,6 @@ def user_provided_handler_function(important, stuff): serious_business(stuff) """ def __init__(self, api, method, path, operation, resolver, - app_security=None, security_schemes=None, validate_responses=False, strict_validation=False, randomize_endpoint=None, validator_map=None, pythonic_params=False, uri_parser_class=None, @@ -88,8 +86,6 @@ def __init__(self, api, method, path, operation, resolver, self._path = path self._operation = operation self._resolver = resolver - self._security = app_security - self._security_schemes = security_schemes self._validate_responses = validate_responses self._strict_validation = strict_validation self._pythonic_params = pythonic_params @@ -106,6 +102,10 @@ def __init__(self, api, method, path, operation, resolver, self._validator_map = dict(VALIDATOR_MAP) self._validator_map.update(validator_map or {}) + @property + def api(self): + return self._api + @property def method(self): """ @@ -377,11 +377,6 @@ def function(self): uri_parsing_decorator = self._uri_parsing_decorator function = uri_parsing_decorator(function) - # NOTE: the security decorator should be applied last to check auth before anything else :-) - security_decorator = self.security_decorator - logger.debug('... Adding security decorator (%r)', security_decorator) - function = security_decorator(function) - function = self._request_response_decorator(function) if UWSGIMetricsCollector.is_available(): # pragma: no cover @@ -390,6 +385,17 @@ def function(self): return function + @property + def _request_response_decorator(self): + """ + Guarantees that instead of the internal representation of the + operation handler response + (connexion.lifecycle.ConnexionRequest) a framework specific + object is returned. + :rtype: types.FunctionType + """ + return RequestResponseDecorator(self.api, self.get_mimetype()) + @property def __content_type_decorator(self): """ diff --git a/connexion/operations/compat.py b/connexion/operations/compat.py deleted file mode 100644 index 8596b66da..000000000 --- a/connexion/operations/compat.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -This is a dummy module for backwards compatibility with < v2.0. -""" -from .secure import * # noqa -from .swagger2 import * # noqa diff --git a/connexion/operations/openapi.py b/connexion/operations/openapi.py index 7f5d0de3a..69c4404c5 100644 --- a/connexion/operations/openapi.py +++ b/connexion/operations/openapi.py @@ -70,9 +70,6 @@ def __init__(self, api, method, path, operation, resolver, path_parameters=None, def component_get(oas3_name): return self.components.get(oas3_name, {}) - # operation overrides globals - security_schemes = component_get('securitySchemes') - app_security = operation.get('security', app_security) uri_parser_class = uri_parser_class or OpenAPIURIParser self._router_controller = operation.get('x-openapi-router-controller') @@ -83,8 +80,6 @@ def component_get(oas3_name): path=path, operation=operation, resolver=resolver, - app_security=app_security, - security_schemes=security_schemes, validate_responses=validate_responses, strict_validation=strict_validation, randomize_endpoint=randomize_endpoint, diff --git a/connexion/operations/secure.py b/connexion/operations/secure.py deleted file mode 100644 index 5dbd90d91..000000000 --- a/connexion/operations/secure.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -This module defines a SecureOperation class, which implements the security handler for an operation. -""" - -import functools -import logging - -from ..decorators.decorator import RequestResponseDecorator - -logger = logging.getLogger("connexion.operations.secure") - -DEFAULT_MIMETYPE = 'application/json' - - -class SecureOperation: - - def __init__(self, api, security, security_schemes): - """ - :param security: list of security rules the application uses by default - :type security: list - :param security_definitions: `Security Definitions Object - `_ - :type security_definitions: dict - """ - self._api = api - self._security = security - self._security_schemes = security_schemes - - @property - def api(self): - return self._api - - @property - def security(self): - return self._security - - @property - def security_schemes(self): - return self._security_schemes - - @property - def security_decorator(self): - """ - Gets the security decorator for operation - - From Swagger Specification: - - **Security Definitions Object** - - A declaration of the security schemes available to be used in the specification. - - This does not enforce the security schemes on the operations and only serves to provide the relevant details - for each scheme. - - - **Operation Object -> security** - - A declaration of which security schemes are applied for this operation. The list of values describes alternative - security schemes that can be used (that is, there is a logical OR between the security requirements). - This definition overrides any declared top-level security. To remove a top-level security declaration, - an empty array can be used. - - - **Security Requirement Object** - - Lists the required security schemes to execute this operation. The object can have multiple security schemes - declared in it which are all required (that is, there is a logical AND between the schemes). - - The name used for each property **MUST** correspond to a security scheme declared in the Security Definitions. - - :rtype: types.FunctionType - """ - logger.debug('... Security: %s', self.security, extra=vars(self)) - if not self.security: - return self._api.security_handler_factory.security_passthrough - - auth_funcs = [] - for security_req in self.security: - if not security_req: - auth_funcs.append(self._api.security_handler_factory.verify_none()) - continue - - sec_req_funcs = {} - oauth = False - for scheme_name, required_scopes in security_req.items(): - security_scheme = self.security_schemes[scheme_name] - - if security_scheme['type'] == 'oauth2': - if oauth: - logger.warning("... multiple OAuth2 security schemes in AND fashion not supported", extra=vars(self)) - break - oauth = True - token_info_func = self._api.security_handler_factory.get_tokeninfo_func(security_scheme) - scope_validate_func = self._api.security_handler_factory.get_scope_validate_func(security_scheme) - if not token_info_func: - logger.warning("... x-tokenInfoFunc missing", extra=vars(self)) - break - - sec_req_funcs[scheme_name] = self._api.security_handler_factory.verify_oauth( - token_info_func, scope_validate_func, required_scopes) - - # Swagger 2.0 - elif security_scheme['type'] == 'basic': - basic_info_func = self._api.security_handler_factory.get_basicinfo_func(security_scheme) - if not basic_info_func: - logger.warning("... x-basicInfoFunc missing", extra=vars(self)) - break - - sec_req_funcs[scheme_name] = self._api.security_handler_factory.verify_basic(basic_info_func) - - # OpenAPI 3.0.0 - elif security_scheme['type'] == 'http': - scheme = security_scheme['scheme'].lower() - if scheme == 'basic': - basic_info_func = self._api.security_handler_factory.get_basicinfo_func(security_scheme) - if not basic_info_func: - logger.warning("... x-basicInfoFunc missing", extra=vars(self)) - break - - sec_req_funcs[scheme_name] = self._api.security_handler_factory.verify_basic(basic_info_func) - elif scheme == 'bearer': - bearer_info_func = self._api.security_handler_factory.get_bearerinfo_func(security_scheme) - if not bearer_info_func: - logger.warning("... x-bearerInfoFunc missing", extra=vars(self)) - break - sec_req_funcs[scheme_name] = self._api.security_handler_factory.verify_bearer(bearer_info_func) - else: - logger.warning("... Unsupported http authorization scheme %s" % scheme, extra=vars(self)) - break - - elif security_scheme['type'] == 'apiKey': - scheme = security_scheme.get('x-authentication-scheme', '').lower() - if scheme == 'bearer': - bearer_info_func = self._api.security_handler_factory.get_bearerinfo_func(security_scheme) - if not bearer_info_func: - logger.warning("... x-bearerInfoFunc missing", extra=vars(self)) - break - sec_req_funcs[scheme_name] = self._api.security_handler_factory.verify_bearer(bearer_info_func) - else: - apikey_info_func = self._api.security_handler_factory.get_apikeyinfo_func(security_scheme) - if not apikey_info_func: - logger.warning("... x-apikeyInfoFunc missing", extra=vars(self)) - break - - sec_req_funcs[scheme_name] = self._api.security_handler_factory.verify_api_key( - apikey_info_func, security_scheme['in'], security_scheme['name'] - ) - - else: - logger.warning("... Unsupported security scheme type %s" % security_scheme['type'], extra=vars(self)) - break - else: - # No break encountered: no missing funcs - if len(sec_req_funcs) == 1: - (func,) = sec_req_funcs.values() - auth_funcs.append(func) - else: - auth_funcs.append(self._api.security_handler_factory.verify_multiple_schemes(sec_req_funcs)) - - return functools.partial(self._api.security_handler_factory.verify_security, auth_funcs) - - def get_mimetype(self): - return DEFAULT_MIMETYPE - - @property - def _request_response_decorator(self): - """ - Guarantees that instead of the internal representation of the - operation handler response - (connexion.lifecycle.ConnexionRequest) a framework specific - object is returned. - :rtype: types.FunctionType - """ - return RequestResponseDecorator(self.api, self.get_mimetype()) diff --git a/connexion/operations/swagger2.py b/connexion/operations/swagger2.py index e89ce1cbd..88767bc2e 100644 --- a/connexion/operations/swagger2.py +++ b/connexion/operations/swagger2.py @@ -27,8 +27,7 @@ class Swagger2Operation(AbstractOperation): """ def __init__(self, api, method, path, operation, resolver, app_produces, app_consumes, - path_parameters=None, app_security=None, security_definitions=None, - definitions=None, parameter_definitions=None, + path_parameters=None, definitions=None, parameter_definitions=None, response_definitions=None, validate_responses=False, strict_validation=False, randomize_endpoint=None, validator_map=None, pythonic_params=False, uri_parser_class=None, pass_context_arg_name=None): @@ -49,11 +48,6 @@ def __init__(self, api, method, path, operation, resolver, app_produces, app_con :type app_consumes: list :param path_parameters: Parameters defined in the path level :type path_parameters: list - :param app_security: list of security rules the application uses by default - :type app_security: list - :param security_definitions: `Security Definitions Object - `_ - :type security_definitions: dict :param definitions: `Definitions Object `_ :type definitions: dict @@ -78,7 +72,6 @@ def __init__(self, api, method, path, operation, resolver, app_produces, app_con name. :type pass_context_arg_name: str|None """ - app_security = operation.get('security', app_security) uri_parser_class = uri_parser_class or Swagger2URIParser self._router_controller = operation.get('x-swagger-router-controller') @@ -89,8 +82,6 @@ def __init__(self, api, method, path, operation, resolver, app_produces, app_con path=path, operation=operation, resolver=resolver, - app_security=app_security, - security_schemes=security_definitions, validate_responses=validate_responses, strict_validation=strict_validation, randomize_endpoint=randomize_endpoint, @@ -130,10 +121,8 @@ def from_spec(cls, spec, api, path, method, resolver, *args, **kwargs): spec.get_operation(path, method), resolver=resolver, path_parameters=spec.get_path_params(path), - app_security=spec.security, app_produces=spec.produces, app_consumes=spec.consumes, - security_definitions=spec.security_definitions, definitions=spec.definitions, parameter_definitions=spec.parameter_definitions, response_definitions=spec.response_definitions, diff --git a/connexion/security/__init__.py b/connexion/security/__init__.py index 7d3325937..136011c5e 100644 --- a/connexion/security/__init__.py +++ b/connexion/security/__init__.py @@ -5,14 +5,4 @@ isort:skip_file """ -# abstract -from .async_security_handler_factory import AbstractAsyncSecurityHandlerFactory # NOQA -from .security_handler_factory import AbstractSecurityHandlerFactory # NOQA - -from ..utils import not_installed_error - -# concrete -try: - from .flask_security_handler_factory import FlaskSecurityHandlerFactory -except ImportError as err: # pragma: no cover - FlaskSecurityHandlerFactory = not_installed_error(err) +from .security_handler_factory import SecurityHandlerFactory # NOQA diff --git a/connexion/security/async_security_handler_factory.py b/connexion/security/async_security_handler_factory.py deleted file mode 100644 index 4eeb927e1..000000000 --- a/connexion/security/async_security_handler_factory.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -This module defines an abstract asynchronous SecurityHandlerFactory which supports the creation of -asynchronous security handlers for coroutine operations. -""" - -import abc -import asyncio -import functools -import logging - -from ..exceptions import OAuthProblem, OAuthResponseProblem, OAuthScopeProblem -from .security_handler_factory import AbstractSecurityHandlerFactory - -logger = logging.getLogger('connexion.api.security') - - -class AbstractAsyncSecurityHandlerFactory(AbstractSecurityHandlerFactory): - def _generic_check(self, func, exception_msg): - need_to_add_context, need_to_add_required_scopes = self._need_to_add_context_or_scopes(func) - - async def wrapper(request, *args, required_scopes=None): - kwargs = {} - if need_to_add_context: - kwargs[self.pass_context_arg_name] = request.context - if need_to_add_required_scopes: - kwargs[self.required_scopes_kw] = required_scopes - token_info = func(*args, **kwargs) - while asyncio.iscoroutine(token_info): - token_info = await token_info - if token_info is self.no_value: - return self.no_value - if token_info is None: - raise OAuthResponseProblem(description=exception_msg, token_response=None) - return token_info - - return wrapper - - def check_oauth_func(self, token_info_func, scope_validate_func): - get_token_info = self._generic_check(token_info_func, 'Provided token is not valid') - need_to_add_context, _ = self._need_to_add_context_or_scopes(scope_validate_func) - - async def wrapper(request, token, required_scopes): - token_info = await get_token_info(request, token, required_scopes=required_scopes) - - # Fallback to 'scopes' for backward compatibility - token_scopes = token_info.get('scope', token_info.get('scopes', '')) - - kwargs = {} - if need_to_add_context: - kwargs[self.pass_context_arg_name] = request.context - validation = scope_validate_func(required_scopes, token_scopes, **kwargs) - while asyncio.iscoroutine(validation): - validation = await validation - if not validation: - raise OAuthScopeProblem( - description='Provided token doesn\'t have the required scope', - required_scopes=required_scopes, - token_scopes=token_scopes - ) - - return token_info - return wrapper - - @classmethod - def verify_security(cls, auth_funcs, function): - @functools.wraps(function) - async def wrapper(request): - token_info = cls.no_value - errors = [] - for func in auth_funcs: - try: - token_info = func(request) - while asyncio.iscoroutine(token_info): - token_info = await token_info - if token_info is not cls.no_value: - break - except Exception as err: - errors.append(err) - - if token_info is cls.no_value: - if errors != []: - cls._raise_most_specific(errors) - else: - logger.info("... No auth provided. Aborting with 401.") - raise OAuthProblem(description='No authorization token provided') - - # Fallback to 'uid' for backward compatibility - request.context['user'] = token_info.get('sub', token_info.get('uid')) - request.context['token_info'] = token_info - return function(request) - - return wrapper - - @abc.abstractmethod - def get_token_info_remote(self, token_info_url): - """ - Return a function which will call `token_info_url` to retrieve token info. - - Returned function must accept oauth token in parameter. - It must return a token_info dict in case of success, None otherwise. - - :param token_info_url: Url to get information about the token - :type token_info_url: str - :rtype: types.FunctionType - """ diff --git a/connexion/security/flask_security_handler_factory.py b/connexion/security/flask_security_handler_factory.py deleted file mode 100644 index 610aa1899..000000000 --- a/connexion/security/flask_security_handler_factory.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -This module defines a Flask-specific SecurityHandlerFactory. -""" - -import requests - -from .security_handler_factory import AbstractSecurityHandlerFactory - -# use connection pool for OAuth tokeninfo -adapter = requests.adapters.HTTPAdapter(pool_connections=100, pool_maxsize=100) -session = requests.Session() -session.mount('http://', adapter) -session.mount('https://', adapter) - - -class FlaskSecurityHandlerFactory(AbstractSecurityHandlerFactory): - def get_token_info_remote(self, token_info_url): - """ - Return a function which will call `token_info_url` to retrieve token info. - - Returned function must accept oauth token in parameter. - It must return a token_info dict in case of success, None otherwise. - - :param token_info_url: Url to get information about the token - :type token_info_url: str - :rtype: types.FunctionType - """ - def wrapper(token): - """ - Retrieve oauth token_info remotely using HTTP - :param token: oauth token from authorization header - :type token: str - :rtype: dict - """ - headers = {'Authorization': f'Bearer {token}'} - token_request = session.get(token_info_url, headers=headers, timeout=5) - if not token_request.ok: - return None - return token_request.json() - return wrapper diff --git a/connexion/security/security_handler_factory.py b/connexion/security/security_handler_factory.py index 5a09888c0..ee2827522 100644 --- a/connexion/security/security_handler_factory.py +++ b/connexion/security/security_handler_factory.py @@ -3,15 +3,16 @@ handlers for operations. """ -import abc +import asyncio import base64 -import functools import http.cookies import logging import os import textwrap import typing as t +import httpx + from ..decorators.parameter import inspect_function_arguments from ..exceptions import (ConnexionException, OAuthProblem, OAuthResponseProblem, OAuthScopeProblem) @@ -20,7 +21,7 @@ logger = logging.getLogger('connexion.api.security') -class AbstractSecurityHandlerFactory(abc.ABC): +class SecurityHandlerFactory: """ get_*_func -> _get_function -> get_function_from_name (name=security function defined in spec) (if url defined instead of a function -> get_token_info_remote) @@ -36,6 +37,7 @@ class AbstractSecurityHandlerFactory(abc.ABC): """ no_value = object() required_scopes_kw = 'required_scopes' + client = None def __init__(self, pass_context_arg_name): self.pass_context_arg_name = pass_context_arg_name @@ -113,12 +115,8 @@ def get_bearerinfo_func(cls, security_definition): return cls._get_function(security_definition, "x-bearerInfoFunc", 'BEARERINFO_FUNC') @staticmethod - def security_passthrough(function): - """ - :type function: types.FunctionType - :rtype: types.FunctionType - """ - return function + async def security_passthrough(request): + return request @staticmethod def security_deny(function): @@ -170,7 +168,7 @@ def get_auth_header_value(request): try: auth_type, value = authorization.split(None, 1) except ValueError: - raise OAuthProblem(description='Invalid authorization header') + raise OAuthProblem(detail='Invalid authorization header') return auth_type.lower(), value def verify_oauth(self, token_info_func, scope_validate_func, required_scopes): @@ -196,7 +194,7 @@ def wrapper(request): try: username, password = base64.b64decode(user_pass).decode('latin1').split(':', 1) except Exception: - raise OAuthProblem(description='Invalid authorization header') + raise OAuthProblem(detail='Invalid authorization header') return check_basic_info_func(request, username, password) @@ -313,17 +311,19 @@ def _need_to_add_context_or_scopes(self, func): def _generic_check(self, func, exception_msg): need_to_add_context, need_to_add_required_scopes = self._need_to_add_context_or_scopes(func) - def wrapper(request, *args, required_scopes=None): + async def wrapper(request, *args, required_scopes=None): kwargs = {} if need_to_add_context: kwargs[self.pass_context_arg_name] = request.context if need_to_add_required_scopes: kwargs[self.required_scopes_kw] = required_scopes token_info = func(*args, **kwargs) + while asyncio.iscoroutine(token_info): + token_info = await token_info if token_info is self.no_value: return self.no_value if token_info is None: - raise OAuthResponseProblem(description=exception_msg, token_response=None) + raise OAuthResponseProblem(detail=exception_msg, token_response=None) return token_info return wrapper @@ -341,8 +341,8 @@ def check_oauth_func(self, token_info_func, scope_validate_func): get_token_info = self._generic_check(token_info_func, 'Provided token is not valid') need_to_add_context, _ = self._need_to_add_context_or_scopes(scope_validate_func) - def wrapper(request, token, required_scopes): - token_info = get_token_info(request, token, required_scopes=required_scopes) + async def wrapper(request, token, required_scopes): + token_info = await get_token_info(request, token, required_scopes=required_scopes) # Fallback to 'scopes' for backward compatibility token_scopes = token_info.get('scope', token_info.get('scopes', '')) @@ -351,43 +351,48 @@ def wrapper(request, token, required_scopes): if need_to_add_context: kwargs[self.pass_context_arg_name] = request.context validation = scope_validate_func(required_scopes, token_scopes, **kwargs) + while asyncio.iscoroutine(validation): + validation = await validation if not validation: raise OAuthScopeProblem( - description='Provided token doesn\'t have the required scope', + detail='Provided token doesn\'t have the required scope', required_scopes=required_scopes, token_scopes=token_scopes - ) + ) return token_info return wrapper @classmethod - def verify_security(cls, auth_funcs, function): - @functools.wraps(function) - def wrapper(request): + def verify_security(cls, auth_funcs): + + async def verify_fn(request): token_info = cls.no_value errors = [] for func in auth_funcs: try: token_info = func(request) + while asyncio.iscoroutine(token_info): + token_info = await token_info if token_info is not cls.no_value: break except Exception as err: errors.append(err) - if token_info is cls.no_value: + else: if errors != []: cls._raise_most_specific(errors) else: logger.info("... No auth provided. Aborting with 401.") - raise OAuthProblem(description='No authorization token provided') + raise OAuthProblem(detail='No authorization token provided') - # Fallback to 'uid' for backward compatibility - request.context['user'] = token_info.get('sub', token_info.get('uid')) - request.context['token_info'] = token_info - return function(request) + request.context.update({ + # Fallback to 'uid' for backward compatibility + 'user': token_info.get('sub', token_info.get('uid')), + 'token_info': token_info + }) - return wrapper + return verify_fn @staticmethod def _raise_most_specific(exceptions: t.List[Exception]) -> None: @@ -409,7 +414,7 @@ def _raise_most_specific(exceptions: t.List[Exception]) -> None: # We only use status code attributes from exceptions # We use 600 as default because 599 is highest valid status code status_to_exc = { - getattr(exc, 'code', getattr(exc, 'status', 600)): exc + getattr(exc, 'status_code', getattr(exc, 'status', 600)): exc for exc in exceptions } if 403 in status_to_exc: @@ -420,7 +425,6 @@ def _raise_most_specific(exceptions: t.List[Exception]) -> None: lowest_status_code = min(status_to_exc) raise status_to_exc[lowest_status_code] - @abc.abstractmethod def get_token_info_remote(self, token_info_url): """ Return a function which will call `token_info_url` to retrieve token info. @@ -432,3 +436,12 @@ def get_token_info_remote(self, token_info_url): :type token_info_url: str :rtype: types.FunctionType """ + async def wrapper(token): + if self.client is None: + self.client = httpx.AsyncClient() + headers = {'Authorization': f'Bearer {token}'} + token_request = await self.client.get(token_info_url, headers=headers, timeout=5) + if token_request.status_code != 200: + return + return token_request.json() + return wrapper diff --git a/setup.py b/setup.py index cc14c0c02..2b5b067ba 100755 --- a/setup.py +++ b/setup.py @@ -29,13 +29,14 @@ def read_version(package): 'importlib-metadata>=1 ; python_version<"3.8"', 'packaging>=20', 'starlette>=0.15,<1', + 'httpx>=0.15,<1', ] swagger_ui_require = 'swagger-ui-bundle>=0.0.2,<0.1' flask_require = [ 'flask>=2,<3', - 'a2wsgi>=1.1,<2', + 'a2wsgi>=1.4,<2', ] tests_require = [ diff --git a/tests/api/test_secure_api.py b/tests/api/test_secure_api.py index 195de9851..1e53cbd66 100644 --- a/tests/api/test_secure_api.py +++ b/tests/api/test_secure_api.py @@ -36,7 +36,6 @@ def test_security(oauth_requests, secure_endpoint_app): assert get_bye_no_auth.status_code == 401 assert get_bye_no_auth.content_type == 'application/problem+json' get_bye_no_auth_reponse = json.loads(get_bye_no_auth.data.decode('utf-8', 'replace')) # type: dict - assert get_bye_no_auth_reponse['title'] == 'Unauthorized' assert get_bye_no_auth_reponse['detail'] == "No authorization token provided" headers = {"Authorization": "Bearer 100"} @@ -49,7 +48,6 @@ def test_security(oauth_requests, secure_endpoint_app): assert get_bye_wrong_scope.status_code == 403 assert get_bye_wrong_scope.content_type == 'application/problem+json' get_bye_wrong_scope_reponse = json.loads(get_bye_wrong_scope.data.decode('utf-8', 'replace')) # type: dict - assert get_bye_wrong_scope_reponse['title'] == 'Forbidden' assert get_bye_wrong_scope_reponse['detail'] == "Provided token doesn't have the required scope" headers = {"Authorization": "Bearer 300"} @@ -57,7 +55,6 @@ def test_security(oauth_requests, secure_endpoint_app): assert get_bye_bad_token.status_code == 401 assert get_bye_bad_token.content_type == 'application/problem+json' get_bye_bad_token_reponse = json.loads(get_bye_bad_token.data.decode('utf-8', 'replace')) # type: dict - assert get_bye_bad_token_reponse['title'] == 'Unauthorized' assert get_bye_bad_token_reponse['detail'] == "Provided token is not valid" response = app_client.get('/v1.0/more-than-one-security-definition') # type: flask.Response diff --git a/tests/conftest.py b/tests/conftest.py index b87672a97..1da100a9d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest from connexion import App -from connexion.security import FlaskSecurityHandlerFactory +from connexion.security import SecurityHandlerFactory from werkzeug.test import Client, EnvironBuilder logging.basicConfig(level=logging.DEBUG) @@ -69,32 +69,36 @@ def f(*args, **kwargs): @pytest.fixture def oauth_requests(monkeypatch): - def fake_get(url, params=None, headers=None, timeout=None): - """ - :type url: str - :type params: dict| None - """ - headers = headers or {} - if url == "https://oauth.example/token_info": - token = headers.get('Authorization', 'invalid').split()[-1] - if token in ["100", "has_myscope"]: - return FakeResponse(200, '{"uid": "test-user", "scope": ["myscope"]}') - if token in ["200", "has_wrongscope"]: - return FakeResponse(200, '{"uid": "test-user", "scope": ["wrongscope"]}') - if token == "has_myscope_otherscope": - return FakeResponse(200, '{"uid": "test-user", "scope": ["myscope", "otherscope"]}') - if token in ["300", "is_not_invalid"]: - return FakeResponse(404, '') - if token == "has_scopes_in_scopes_with_s": - return FakeResponse(200, '{"uid": "test-user", "scopes": ["myscope", "otherscope"]}') - return url - - monkeypatch.setattr('connexion.security.flask_security_handler_factory.session.get', fake_get) + + class FakeClient: + + @staticmethod + async def get(url, params=None, headers=None, timeout=None): + """ + :type url: str + :type params: dict| None + """ + headers = headers or {} + if url == "https://oauth.example/token_info": + token = headers.get('Authorization', 'invalid').split()[-1] + if token in ["100", "has_myscope"]: + return FakeResponse(200, '{"uid": "test-user", "scope": ["myscope"]}') + if token in ["200", "has_wrongscope"]: + return FakeResponse(200, '{"uid": "test-user", "scope": ["wrongscope"]}') + if token == "has_myscope_otherscope": + return FakeResponse(200, '{"uid": "test-user", "scope": ["myscope", "otherscope"]}') + if token in ["300", "is_not_invalid"]: + return FakeResponse(404, '') + if token == "has_scopes_in_scopes_with_s": + return FakeResponse(200, '{"uid": "test-user", "scopes": ["myscope", "otherscope"]}') + return url + + monkeypatch.setattr(SecurityHandlerFactory, 'client', FakeClient()) @pytest.fixture def security_handler_factory(): - security_handler_factory = FlaskSecurityHandlerFactory(None) + security_handler_factory = SecurityHandlerFactory(None) yield security_handler_factory diff --git a/tests/decorators/test_security.py b/tests/decorators/test_security.py index 60ab26260..14a624cb4 100644 --- a/tests/decorators/test_security.py +++ b/tests/decorators/test_security.py @@ -6,6 +6,7 @@ from connexion.exceptions import (BadRequestProblem, ConnexionException, OAuthProblem, OAuthResponseProblem, OAuthScopeProblem) +from connexion.security import SecurityHandlerFactory def test_get_tokeninfo_url(monkeypatch, security_handler_factory): @@ -43,10 +44,10 @@ def somefunc(token): assert wrapped_func(request) is security_handler_factory.no_value -def test_verify_oauth_scopes_remote(monkeypatch, security_handler_factory): +async def test_verify_oauth_scopes_remote(monkeypatch, security_handler_factory): tokeninfo = dict(uid="foo", scope="scope1 scope2") - def get_tokeninfo_response(*args, **kwargs): + async def get_tokeninfo_response(*args, **kwargs): tokeninfo_response = requests.Response() tokeninfo_response.status_code = requests.codes.ok tokeninfo_response._content = json.dumps(tokeninfo).encode() @@ -58,25 +59,25 @@ def get_tokeninfo_response(*args, **kwargs): request = MagicMock() request.headers = {"Authorization": "Bearer 123"} - session = MagicMock() - session.get = get_tokeninfo_response - monkeypatch.setattr('connexion.security.flask_security_handler_factory.session', session) + client = MagicMock() + client.get = get_tokeninfo_response + monkeypatch.setattr(SecurityHandlerFactory, 'client', client) with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"): - wrapped_func(request) + await wrapped_func(request) tokeninfo["scope"] += " admin" - assert wrapped_func(request) is not None + assert await wrapped_func(request) is not None tokeninfo["scope"] = ["foo", "bar"] with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"): - wrapped_func(request) + await wrapped_func(request) tokeninfo["scope"].append("admin") - assert wrapped_func(request) is not None + assert await wrapped_func(request) is not None -def test_verify_oauth_invalid_local_token_response_none(security_handler_factory): +async def test_verify_oauth_invalid_local_token_response_none(security_handler_factory): def somefunc(token): return None @@ -86,10 +87,10 @@ def somefunc(token): request.headers = {"Authorization": "Bearer 123"} with pytest.raises(OAuthResponseProblem): - wrapped_func(request) + await wrapped_func(request) -def test_verify_oauth_scopes_local(security_handler_factory): +async def test_verify_oauth_scopes_local(security_handler_factory): tokeninfo = dict(uid="foo", scope="scope1 scope2") def token_info(token): @@ -101,17 +102,17 @@ def token_info(token): request.headers = {"Authorization": "Bearer 123"} with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"): - wrapped_func(request) + await wrapped_func(request) tokeninfo["scope"] += " admin" - assert wrapped_func(request) is not None + assert await wrapped_func(request) is not None tokeninfo["scope"] = ["foo", "bar"] with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"): - wrapped_func(request) + await wrapped_func(request) tokeninfo["scope"].append("admin") - assert wrapped_func(request) is not None + assert await wrapped_func(request) is not None def test_verify_basic_missing_auth_header(security_handler_factory): @@ -168,7 +169,7 @@ def apikey_info(apikey, required_scopes=None): assert wrapped_func(request) is not None -def test_multiple_schemes(security_handler_factory): +async def test_multiple_schemes(security_handler_factory): def apikey1_info(apikey, required_scopes=None): if apikey == 'foobar': return {'sub': 'foo'} @@ -195,7 +196,7 @@ def apikey2_info(apikey, required_scopes=None): request = MagicMock() request.headers = {"X-Auth-2": 'bar'} - assert wrapped_func(request) is security_handler_factory.no_value + assert await wrapped_func(request) is security_handler_factory.no_value # Supplying both keys does succeed request = MagicMock() @@ -208,17 +209,16 @@ def apikey2_info(apikey, required_scopes=None): 'key1': {'sub': 'foo'}, 'key2': {'sub': 'bar'}, } - assert wrapped_func(request) == expected_token_info + assert await wrapped_func(request) == expected_token_info -def test_verify_security_oauthproblem(security_handler_factory): +async def test_verify_security_oauthproblem(security_handler_factory): """Tests whether verify_security raises an OAuthProblem if there are no auth_funcs.""" - func_to_secure = MagicMock(return_value='func') - secured_func = security_handler_factory.verify_security([], func_to_secure) + security_func = security_handler_factory.verify_security([], []) request = MagicMock() with pytest.raises(OAuthProblem) as exc_info: - secured_func(request) + await security_func(request) assert str(exc_info.value) == '401 Unauthorized: No authorization token provided' diff --git a/tests/fixtures/secure_api/swagger.yaml b/tests/fixtures/secure_api/swagger.yaml index 326d02aad..acad2a6fb 100644 --- a/tests/fixtures/secure_api/swagger.yaml +++ b/tests/fixtures/secure_api/swagger.yaml @@ -36,3 +36,4 @@ paths: description: Name of the person to greet. required: true type: string + format: path diff --git a/tests/fixtures/secure_endpoint/swagger.yaml b/tests/fixtures/secure_endpoint/swagger.yaml index 06e9d8315..6dfafc362 100644 --- a/tests/fixtures/secure_endpoint/swagger.yaml +++ b/tests/fixtures/secure_endpoint/swagger.yaml @@ -106,7 +106,7 @@ paths: required: true type: string - /byesecure-jwt/: + /byesecure-jwt/{name}: get: summary: Generate goodbye description: "" diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 28a0a01a9..5db57fa92 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,6 +1,5 @@ import pytest from connexion.middleware import ConnexionMiddleware -from connexion.middleware.routing import CONNEXION_CONTEXT from starlette.datastructures import MutableHeaders from conftest import SPECS, build_app_from_fixture @@ -13,7 +12,7 @@ def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): - operation_id = scope['extensions'][CONNEXION_CONTEXT]['operation_id'] + operation_id = scope['extensions']['connexion_routing']['operation_id'] async def patched_send(message): if message["type"] != "http.response.start": diff --git a/tests/test_mock.py b/tests/test_mock.py index f23db4afc..0b2bbace7 100644 --- a/tests/test_mock.py +++ b/tests/test_mock.py @@ -24,8 +24,6 @@ def test_mock_resolver_default(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -57,8 +55,6 @@ def test_mock_resolver_numeric(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -96,8 +92,6 @@ def test_mock_resolver_example(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -133,8 +127,6 @@ def test_mock_resolver_example_nested_in_object(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -168,8 +160,6 @@ def test_mock_resolver_example_nested_in_list(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -274,8 +264,6 @@ def test_mock_resolver_no_example_nested_in_object(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -334,8 +322,6 @@ def test_mock_resolver_no_examples(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -363,8 +349,6 @@ def test_mock_resolver_notimplemented(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) @@ -381,8 +365,6 @@ def test_mock_resolver_notimplemented(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions={}, resolver=resolver) diff --git a/tests/test_operation2.py b/tests/test_operation2.py index 692f5fbb4..ae0efc33e 100644 --- a/tests/test_operation2.py +++ b/tests/test_operation2.py @@ -9,6 +9,7 @@ from connexion.apis.flask_api import Jsonifier from connexion.exceptions import InvalidSpecification from connexion.json_schema import resolve_refs +from connexion.middleware.security import SecurityOperation from connexion.operations import Swagger2Operation from connexion.resolver import Resolver @@ -280,10 +281,6 @@ def make_operation(op, definitions=True, parameters=True): def test_operation(api, security_handler_factory): - verify_oauth = mock.MagicMock(return_value='verify_oauth_result') - security_handler_factory.verify_oauth = verify_oauth - security_handler_factory.get_token_info_remote = mock.MagicMock(return_value='get_token_info_remote_result') - op_spec = make_operation(OPERATION1) operation = Swagger2Operation(api=api, method='GET', @@ -292,29 +289,34 @@ def test_operation(api, security_handler_factory): operation=op_spec, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions=SECURITY_DEFINITIONS_REMOTE, definitions=DEFINITIONS, parameter_definitions=PARAMETER_DEFINITIONS, resolver=Resolver()) - assert isinstance(operation.function, types.FunctionType) - - security_decorator = operation.security_decorator - assert len(security_decorator.args[0]) == 1 - assert security_decorator.args[0][0] == 'verify_oauth_result' - verify_oauth.assert_called_with('get_token_info_remote_result', security_handler_factory.validate_scope, ['uid']) - security_handler_factory.get_token_info_remote.assert_called_with('https://oauth.example/token_info') assert operation.method == 'GET' assert operation.produces == ['application/json'] assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth': ['uid']}] expected_body_schema = op_spec["parameters"][0]["schema"] expected_body_schema.update({'definitions': DEFINITIONS}) assert operation.body_schema == expected_body_schema +def test_operation_remote_token_info(security_handler_factory): + verify_oauth = mock.MagicMock(return_value='verify_oauth_result') + security_handler_factory.verify_oauth = verify_oauth + security_handler_factory.get_token_info_remote = mock.MagicMock(return_value='get_token_info_remote_result') + + SecurityOperation(security_handler_factory=security_handler_factory, + security=[{'oauth': ['uid']}], + security_schemes=SECURITY_DEFINITIONS_REMOTE) + + verify_oauth.assert_called_with('get_token_info_remote_result', + security_handler_factory.validate_scope, + ['uid']) + security_handler_factory.get_token_info_remote.assert_called_with('https://oauth.example/token_info') + + def test_operation_array(api): op_spec = make_operation(OPERATION7) operation = Swagger2Operation(api=api, @@ -324,8 +326,6 @@ def test_operation_array(api): operation=op_spec, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions=SECURITY_DEFINITIONS_REMOTE, definitions=DEFINITIONS, parameter_definitions=PARAMETER_DEFINITIONS, resolver=Resolver()) @@ -334,7 +334,7 @@ def test_operation_array(api): assert operation.method == 'GET' assert operation.produces == ['application/json'] assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth': ['uid']}] + expected_body_schema = { 'type': 'array', 'items': DEFINITIONS["new_stack"], @@ -352,8 +352,6 @@ def test_operation_composed_definition(api): operation=op_spec, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions=SECURITY_DEFINITIONS_REMOTE, definitions=DEFINITIONS, parameter_definitions=PARAMETER_DEFINITIONS, resolver=Resolver()) @@ -362,75 +360,32 @@ def test_operation_composed_definition(api): assert operation.method == 'GET' assert operation.produces == ['application/json'] assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth': ['uid']}] + expected_body_schema = op_spec["parameters"][0]["schema"] expected_body_schema.update({'definitions': DEFINITIONS}) assert operation.body_schema == expected_body_schema -def test_operation_local_security_oauth2(api): +def test_operation_local_security_oauth2(security_handler_factory): verify_oauth = mock.MagicMock(return_value='verify_oauth_result') - api.security_handler_factory.verify_oauth = verify_oauth + security_handler_factory.verify_oauth = verify_oauth - op_spec = make_operation(OPERATION8) - operation = Swagger2Operation(api=api, - method='GET', - path='endpoint', - path_parameters=[], - operation=op_spec, - app_produces=['application/json'], - app_consumes=['application/json'], - app_security=[], - security_definitions=SECURITY_DEFINITIONS_LOCAL, - definitions=DEFINITIONS, - parameter_definitions=PARAMETER_DEFINITIONS, - resolver=Resolver()) - assert isinstance(operation.function, types.FunctionType) - security_decorator = operation.security_decorator - assert len(security_decorator.args[0]) == 1 - assert security_decorator.args[0][0] == 'verify_oauth_result' - verify_oauth.assert_called_with(math.ceil, api.security_handler_factory.validate_scope, ['uid']) + SecurityOperation(security_handler_factory=security_handler_factory, + security=[{'oauth': ['uid']}], + security_schemes=SECURITY_DEFINITIONS_LOCAL) - assert operation.method == 'GET' - assert operation.produces == ['application/json'] - assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth': ['uid']}] - expected_body_schema = op_spec["parameters"][0]["schema"] - expected_body_schema.update({'definitions': DEFINITIONS}) - assert operation.body_schema == expected_body_schema + verify_oauth.assert_called_with(math.ceil, security_handler_factory.validate_scope, ['uid']) -def test_operation_local_security_duplicate_token_info(api): +def test_operation_local_security_duplicate_token_info(security_handler_factory): verify_oauth = mock.MagicMock(return_value='verify_oauth_result') - api.security_handler_factory.verify_oauth = verify_oauth - - op_spec = make_operation(OPERATION8) - operation = Swagger2Operation(api=api, - method='GET', - path='endpoint', - path_parameters=[], - operation=op_spec, - app_produces=['application/json'], - app_consumes=['application/json'], - app_security=[], - security_definitions=SECURITY_DEFINITIONS_BOTH, - definitions=DEFINITIONS, - parameter_definitions=PARAMETER_DEFINITIONS, - resolver=Resolver()) - assert isinstance(operation.function, types.FunctionType) + security_handler_factory.verify_oauth = verify_oauth - security_decorator = operation.security_decorator - assert len(security_decorator.args[0]) == 1 - assert security_decorator.args[0][0] == 'verify_oauth_result' - verify_oauth.call_args.assert_called_with(math.ceil, api.security_handler_factory.validate_scope, ['uid']) + SecurityOperation(security_handler_factory, + security=[{'oauth': ['uid']}], + security_schemes=SECURITY_DEFINITIONS_BOTH) - assert operation.method == 'GET' - assert operation.produces == ['application/json'] - assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth': ['uid']}] - expected_body_schema = op_spec["parameters"][0]["schema"] - expected_body_schema.update({'definitions': DEFINITIONS}) - assert operation.body_schema == expected_body_schema + verify_oauth.call_args.assert_called_with(math.ceil, security_handler_factory.validate_scope) def test_multi_body(api): @@ -443,8 +398,6 @@ def test_multi_body(api): operation=op_spec, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions=DEFINITIONS, parameter_definitions=PARAMETER_DEFINITIONS, resolver=Resolver()) @@ -455,58 +408,27 @@ def test_multi_body(api): assert repr(exception) == """""" -def test_no_token_info(api): - op_spec = make_operation(OPERATION1) - operation = Swagger2Operation(api=api, - method='GET', - path='endpoint', - path_parameters=[], - operation=op_spec, - app_produces=['application/json'], - app_consumes=['application/json'], - app_security=SECURITY_DEFINITIONS_WO_INFO, - security_definitions=SECURITY_DEFINITIONS_WO_INFO, - definitions=DEFINITIONS, - parameter_definitions=PARAMETER_DEFINITIONS, - resolver=Resolver()) - assert isinstance(operation.function, types.FunctionType) - - security_decorator = operation.security_decorator - assert len(security_decorator.args[0]) == 0 - - assert operation.method == 'GET' - assert operation.produces == ['application/json'] - assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth': ['uid']}] - - expected_body_schema = {'definitions': DEFINITIONS} - expected_body_schema.update(DEFINITIONS["new_stack"]) - assert operation.body_schema == expected_body_schema +def test_no_token_info(security_handler_factory): + SecurityOperation(security_handler_factory=security_handler_factory, + security=[{'oauth': ['uid']}], + security_schemes=SECURITY_DEFINITIONS_WO_INFO) -def test_multiple_security_schemes_and(api): +def test_multiple_security_schemes_and(security_handler_factory): """Tests an operation with multiple security schemes in AND fashion.""" def return_api_key_name(func, in_, name): return name verify_api_key = mock.MagicMock(side_effect=return_api_key_name) - api.security_handler_factory.verify_api_key = verify_api_key + security_handler_factory.verify_api_key = verify_api_key verify_multiple = mock.MagicMock(return_value='verify_multiple_result') - api.security_handler_factory.verify_multiple_schemes = verify_multiple + security_handler_factory.verify_multiple_schemes = verify_multiple + + security = [{'key1': [], 'key2': []}] + + SecurityOperation(security_handler_factory=security_handler_factory, + security=security, + security_schemes=SECURITY_DEFINITIONS_2_KEYS) - op_spec = make_operation(OPERATION9) - operation = Swagger2Operation(api=api, - method='GET', - path='endpoint', - path_parameters=[], - operation=op_spec, - app_produces=['application/json'], - app_consumes=['application/json'], - app_security=SECURITY_DEFINITIONS_2_KEYS, - security_definitions=SECURITY_DEFINITIONS_2_KEYS, - definitions=DEFINITIONS, - parameter_definitions=PARAMETER_DEFINITIONS, - resolver=Resolver()) - assert isinstance(operation.function, types.FunctionType) assert verify_api_key.call_count == 2 verify_api_key.assert_any_call(math.ceil, 'header', 'X-Auth-1') verify_api_key.assert_any_call(math.ceil, 'header', 'X-Auth-2') @@ -514,50 +436,23 @@ def return_api_key_name(func, in_, name): # to result of security_handler_factory.verify_api_key() verify_multiple.assert_called_with({'key1': 'X-Auth-1', 'key2': 'X-Auth-2'}) - security_decorator = operation.security_decorator - assert len(security_decorator.args[0]) == 1 - assert security_decorator.args[0][0] == 'verify_multiple_result' - - assert operation.method == 'GET' - assert operation.produces == ['application/json'] - assert operation.consumes == ['application/json'] - assert operation.security == [{'key1': [], 'key2': []}] - -def test_multiple_oauth_in_and(api, caplog): +def test_multiple_oauth_in_and(security_handler_factory, caplog): """Tests an operation with multiple oauth security schemes in AND fashion. These should be ignored and raise a warning. """ caplog.set_level(logging.WARNING, logger="connexion.operations.secure") verify_oauth = mock.MagicMock(return_value='verify_oauth_result') - api.security_handler_factory.verify_oauth = verify_oauth + security_handler_factory.verify_oauth = verify_oauth - op_spec = make_operation(OPERATION10) - operation = Swagger2Operation(api=api, - method='GET', - path='endpoint', - path_parameters=[], - operation=op_spec, - app_produces=['application/json'], - app_consumes=['application/json'], - app_security=SECURITY_DEFINITIONS_2_OAUTH, - security_definitions=SECURITY_DEFINITIONS_2_OAUTH, - definitions=DEFINITIONS, - parameter_definitions=PARAMETER_DEFINITIONS, - resolver=Resolver()) - assert isinstance(operation.function, types.FunctionType) + security = [{'oauth_1': ['uid'], 'oauth_2': ['uid']}] - security_decorator = operation.security_decorator - assert len(security_decorator.args[0]) == 0 - assert security_decorator.args[0] == [] + SecurityOperation(security_handler_factory=security_handler_factory, + security=security, + security_schemes=SECURITY_DEFINITIONS_2_OAUTH) assert '... multiple OAuth2 security schemes in AND fashion not supported' in caplog.text - assert operation.method == 'GET' - assert operation.produces == ['application/json'] - assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth_1': ['uid'], 'oauth_2': ['uid']}] - def test_parameter_reference(api): op_spec = make_operation(OPERATION3, definitions=False) @@ -568,8 +463,6 @@ def test_parameter_reference(api): operation=op_spec, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=Resolver()) @@ -582,8 +475,7 @@ def test_default(api): Swagger2Operation( api=api, method='GET', path='endpoint', path_parameters=[], operation=op_spec, app_produces=['application/json'], - app_consumes=['application/json'], app_security=[], - security_definitions={}, definitions=DEFINITIONS, + app_consumes=['application/json'], definitions=DEFINITIONS, parameter_definitions=PARAMETER_DEFINITIONS, resolver=Resolver() ) op_spec = make_operation(OPERATION6, parameters=False) @@ -596,8 +488,7 @@ def test_default(api): Swagger2Operation( api=api, method='POST', path='endpoint', path_parameters=[], operation=op_spec, app_produces=['application/json'], - app_consumes=['application/json'], app_security=[], - security_definitions={}, definitions=DEFINITIONS, + app_consumes=['application/json'], definitions=DEFINITIONS, parameter_definitions={}, resolver=Resolver() ) @@ -620,35 +511,18 @@ def test_get_path_parameter_types(api): assert {'int_path': 'int', 'string_path': 'string', 'path_path': 'path'} == operation.get_path_parameter_types() -def test_oauth_scopes_in_or(api): +def test_oauth_scopes_in_or(security_handler_factory): """Tests whether an OAuth security scheme with 2 different possible scopes is correctly handled.""" verify_oauth = mock.MagicMock(return_value='verify_oauth_result') - api.security_handler_factory.verify_oauth = verify_oauth + security_handler_factory.verify_oauth = verify_oauth + + security = [{'oauth': ['myscope']}, {'oauth': ['myscope2']}] + + SecurityOperation(security_handler_factory=security_handler_factory, + security=security, + security_schemes=SECURITY_DEFINITIONS_LOCAL) - op_spec = make_operation(OPERATION11) - operation = Swagger2Operation(api=api, - method='GET', - path='endpoint', - path_parameters=[], - operation=op_spec, - app_produces=['application/json'], - app_consumes=['application/json'], - app_security=[], - security_definitions=SECURITY_DEFINITIONS_LOCAL, - definitions=DEFINITIONS, - parameter_definitions=PARAMETER_DEFINITIONS, - resolver=Resolver()) - assert isinstance(operation.function, types.FunctionType) - security_decorator = operation.security_decorator - assert len(security_decorator.args[0]) == 2 - assert security_decorator.args[0][0] == 'verify_oauth_result' - assert security_decorator.args[0][1] == 'verify_oauth_result' verify_oauth.assert_has_calls([ - mock.call(math.ceil, api.security_handler_factory.validate_scope, ['myscope']), - mock.call(math.ceil, api.security_handler_factory.validate_scope, ['myscope2']), + mock.call(math.ceil, security_handler_factory.validate_scope, ['myscope']), + mock.call(math.ceil, security_handler_factory.validate_scope, ['myscope2']), ]) - - assert operation.method == 'GET' - assert operation.produces == ['application/json'] - assert operation.consumes == ['application/json'] - assert operation.security == [{'oauth': ['myscope']}, {'oauth': ['myscope2']}] diff --git a/tests/test_resolver.py b/tests/test_resolver.py index ece8a53b7..e92e7f9f2 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -55,8 +55,6 @@ def test_standard_resolve_x_router_controller(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=Resolver()) @@ -74,8 +72,6 @@ def test_relative_resolve_x_router_controller(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RelativeResolver('root_path')) @@ -92,8 +88,6 @@ def test_relative_resolve_operation_id(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RelativeResolver('fakeapi')) @@ -111,8 +105,6 @@ def test_relative_resolve_operation_id_with_module(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RelativeResolver(fakeapi)) @@ -129,8 +121,6 @@ def test_resty_resolve_operation_id(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -148,8 +138,6 @@ def test_resty_resolve_x_router_controller_with_operation_id(): }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -164,8 +152,6 @@ def test_resty_resolve_x_router_controller_without_operation_id(): operation={'x-swagger-router-controller': 'fakeapi.hello'}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -180,8 +166,6 @@ def test_resty_resolve_with_default_module_name(): operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -196,8 +180,6 @@ def test_resty_resolve_with_default_module_name_nested(): operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -212,8 +194,6 @@ def test_resty_resolve_with_default_module_name_lowercase_verb(): operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -227,8 +207,6 @@ def test_resty_resolve_with_default_module_name_lowercase_verb_nested(): operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -243,8 +221,6 @@ def test_resty_resolve_with_default_module_name_will_translate_dashes_in_resourc operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -259,8 +235,6 @@ def test_resty_resolve_with_default_module_name_can_resolve_api_root(): operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -275,8 +249,6 @@ def test_resty_resolve_with_default_module_name_will_resolve_resource_root_get_a operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -293,8 +265,6 @@ def test_resty_resolve_with_default_module_name_and_x_router_controller_will_res }, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi')) @@ -309,8 +279,6 @@ def test_resty_resolve_with_default_module_name_will_resolve_resource_root_as_co operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi', 'api_list')) @@ -325,8 +293,6 @@ def test_resty_resolve_with_default_module_name_will_resolve_resource_root_post_ operation={}, app_produces=['application/json'], app_consumes=['application/json'], - app_security=[], - security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS, resolver=RestyResolver('fakeapi'))