Skip to content

Commit

Permalink
Merge pull request #143 from rafaelcaricio/using-api-to-auth-404
Browse files Browse the repository at this point in the history
Auth 404 errors option
  • Loading branch information
jmcs committed Feb 11, 2016
2 parents 86b98d1 + 8c3ad48 commit 812ed1c
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 79 deletions.
22 changes: 20 additions & 2 deletions connexion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
import logging
import pathlib
import yaml
import werkzeug.exceptions
from .operation import Operation
from . import utils
from . import resolver
from .handlers import AuthErrorHandler

MODULE_PATH = pathlib.Path(__file__).absolute().parent
SWAGGER_UI_PATH = MODULE_PATH / 'vendor' / 'swagger-ui'
Expand All @@ -34,14 +36,15 @@ class Api:
"""

def __init__(self, swagger_yaml_path, base_url=None, arguments=None, swagger_ui=None, swagger_path=None,
swagger_url=None, validate_responses=False, resolver=resolver.Resolver()):
swagger_url=None, validate_responses=False, resolver=resolver.Resolver(), auth_all_paths=False):
"""
:type swagger_yaml_path: pathlib.Path
:type base_url: str | None
:type arguments: dict | None
:type swagger_ui: bool
:type swagger_path: string | None
:type swagger_url: string | None
:type auth_all_paths: bool
:param resolver: Callable that maps operationID to a function
"""
self.swagger_yaml_path = pathlib.Path(swagger_yaml_path)
Expand All @@ -50,7 +53,8 @@ def __init__(self, swagger_yaml_path, base_url=None, arguments=None, swagger_ui=
'arguments': arguments,
'swagger_ui': swagger_ui,
'swagger_path': swagger_path,
'swagger_url': swagger_url})
'swagger_url': swagger_url,
'auth_all_paths': auth_all_paths})
arguments = arguments or {}
with swagger_yaml_path.open() as swagger_yaml:
swagger_template = swagger_yaml.read()
Expand Down Expand Up @@ -92,8 +96,12 @@ def __init__(self, swagger_yaml_path, base_url=None, arguments=None, swagger_ui=
self.add_swagger_json()
if swagger_ui:
self.add_swagger_ui()

self.add_paths()

if auth_all_paths:
self.add_auth_on_not_found()

def add_operation(self, method, path, swagger_operation):
"""
Adds one operation to the api.
Expand Down Expand Up @@ -137,6 +145,16 @@ def add_paths(self, paths=None):
except Exception: # pylint: disable= W0703
logger.exception('Failed to add operation for %s %s%s', method.upper(), self.base_url, path)

def add_auth_on_not_found(self):
"""
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(werkzeug.exceptions.NotFound(), security=self.security,
security_definitions=self.security_definitions)
endpoint_name = "{name}_not_found".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/<path:invalid_path>', endpoint_name, not_found_error.function)

def add_swagger_json(self):
"""
Adds swagger json to {base_url}/swagger.json
Expand Down
17 changes: 12 additions & 5 deletions connexion/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@


class App:
def __init__(self, import_name, port=None, specification_dir='', server=None, arguments=None, debug=False,
swagger_ui=True, swagger_path=None, swagger_url=None):
def __init__(self, import_name, port=None, specification_dir='', server=None, arguments=None, auth_all_paths=False,
debug=False, swagger_ui=True, swagger_path=None, swagger_url=None):
"""
:param import_name: the name of the application package
:type import_name: str
Expand All @@ -36,6 +36,8 @@ def __init__(self, import_name, port=None, specification_dir='', server=None, ar
:type server: str | None
:param arguments: arguments to replace on the specification
:type arguments: dict | None
:param auth_all_paths: whether to authenticate not defined paths
:type auth_all_paths: bool
:param debug: include debugging information
:type debug: bool
:param swagger_ui: whether to include swagger ui or not
Expand Down Expand Up @@ -71,6 +73,7 @@ def __init__(self, import_name, port=None, specification_dir='', server=None, ar
self.swagger_ui = swagger_ui
self.swagger_path = swagger_path
self.swagger_url = swagger_url
self.auth_all_paths = auth_all_paths

@staticmethod
def common_error_handler(exception):
Expand All @@ -81,8 +84,8 @@ def common_error_handler(exception):
exception = werkzeug.exceptions.InternalServerError()
return problem(title=exception.name, detail=exception.description, status=exception.code)

def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None, swagger_path=None,
swagger_url=None, validate_responses=False, resolver=Resolver()):
def add_api(self, swagger_file, base_path=None, arguments=None, auth_all_paths=None, swagger_ui=None,
swagger_path=None, swagger_url=None, validate_responses=False, resolver=Resolver()):
"""
Adds an API to the application based on a swagger file
Expand All @@ -92,6 +95,8 @@ def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None,
:type base_path: str | None
:param arguments: api version specific arguments to replace on the specification
:type arguments: dict | None
:param auth_all_paths: whether to authenticate not defined paths
:type auth_all_paths: bool
:param swagger_ui: whether to include swagger ui or not
:type swagger_ui: bool
:param swagger_path: path to swagger-ui directory
Expand All @@ -109,6 +114,7 @@ def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None,
swagger_ui = swagger_ui if swagger_ui is not None else self.swagger_ui
swagger_path = swagger_path if swagger_path is not None else self.swagger_path
swagger_url = swagger_url if swagger_url is not None else self.swagger_url
auth_all_paths = auth_all_paths if auth_all_paths is not None else self.auth_all_paths
logger.debug('Adding API: %s', swagger_file)
# TODO test if base_url starts with an / (if not none)
arguments = arguments or dict()
Expand All @@ -120,7 +126,8 @@ def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None,
swagger_path=swagger_path,
swagger_url=swagger_url,
resolver=resolver,
validate_responses=validate_responses)
validate_responses=validate_responses,
auth_all_paths=auth_all_paths)
self.app.register_blueprint(api.blueprint)
return api

Expand Down
43 changes: 43 additions & 0 deletions connexion/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

import logging
from .operation import SecureOperation
from .problem import problem

logger = logging.getLogger('connexion.handlers')


class AuthErrorHandler(SecureOperation):
"""
Wraps an error with authentication.
"""

def __init__(self, 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
<https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object>`_
:type security_definitions: dict
"""
self.exception = exception
SecureOperation.__init__(self, 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))
return security_decorator(self.handle)

def handle(self, *args, **kwargs):
"""
Actual handler for the execution after authentication.
"""
return problem(title=self.exception.name, detail=self.exception.description, status=self.exception.code)
129 changes: 71 additions & 58 deletions connexion/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,76 @@
logger = logging.getLogger('connexion.operation')


class Operation:
class SecureOperation:
def __init__(self, security, security_definitions):
"""
:param security: list of security rules the application uses by default
:type security: list
:param security_definitions: `Security Definitions Object
<https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object>`_
:type security_definitions: dict
"""
self.security = security
self.security_definitions = security_definitions

@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.
**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 self.security:
if len(self.security) > 1:
logger.warning("... More than one security requirement defined. **IGNORING SECURITY REQUIREMENTS**",
extra=vars(self))
return security_passthrough

security = self.security[0] # type: dict
# the following line gets the first (and because of the previous condition only) scheme and scopes
# from the operation's security requirements

scheme_name, scopes = next(iter(security.items())) # type: str, list
security_definition = self.security_definitions[scheme_name]
if security_definition['type'] == 'oauth2':
token_info_url = security_definition.get('x-tokenInfoUrl', os.getenv('HTTP_TOKENINFO_URL'))
if token_info_url:
scopes = set(scopes) # convert scopes to set because this is needed for verify_oauth
return functools.partial(verify_oauth, token_info_url, scopes)
else:
logger.warning("... OAuth2 token info URL missing. **IGNORING SECURITY REQUIREMENTS**",
extra=vars(self))
elif security_definition['type'] in ('apiKey', 'basic'):
logger.debug(
"... Security type '%s' not natively supported by Connexion; you should handle it yourself",
security_definition['type'], extra=vars(self))
else:
logger.warning("... Security type '%s' unknown. **IGNORING SECURITY REQUIREMENTS**",
security_definition['type'], extra=vars(self))

# if we don't know how to handle the security or it's not defined we will usa a passthrough decorator
return security_passthrough


class Operation(SecureOperation):
"""
A single API operation on a path.
"""
Expand Down Expand Up @@ -261,7 +330,7 @@ def function(self):
function = validation_decorator(function)

# NOTE: the security decorator should be applied last to check auth before anything else :-)
security_decorator = self.__security_decorator
security_decorator = self.security_decorator
logger.debug('... Adding security decorator (%r)', security_decorator, extra=vars(self))
function = security_decorator(function)

Expand Down Expand Up @@ -302,62 +371,6 @@ def __content_type_decorator(self):
else:
return BaseSerializer()

@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.
**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 self.security:
if len(self.security) > 1:
logger.warning("... More than one security requirement defined. **IGNORING SECURITY REQUIREMENTS**",
extra=vars(self))
return security_passthrough

security = self.security[0] # type: dict
# the following line gets the first (and because of the previous condition only) scheme and scopes
# from the operation's security requirements

scheme_name, scopes = next(iter(security.items())) # type: str, list
security_definition = self.security_definitions[scheme_name]
if security_definition['type'] == 'oauth2':
token_info_url = security_definition.get('x-tokenInfoUrl', os.getenv('HTTP_TOKENINFO_URL'))
if token_info_url:
scopes = set(scopes) # convert scopes to set because this is needed for verify_oauth
return functools.partial(verify_oauth, token_info_url, scopes)
else:
logger.warning("... OAuth2 token info URL missing. **IGNORING SECURITY REQUIREMENTS**",
extra=vars(self))
elif security_definition['type'] in ('apiKey', 'basic'):
logger.debug(
"... Security type '%s' not natively supported by Connexion; you should handle it yourself",
security_definition['type'], extra=vars(self))
else:
logger.warning("... Security type '%s' unknown. **IGNORING SECURITY REQUIREMENTS**",
security_definition['type'], extra=vars(self))

# if we don't know how to handle the security or it's not defined we will usa a passthrough decorator
return security_passthrough

@property
def __validation_decorators(self):
"""
Expand Down
13 changes: 6 additions & 7 deletions connexion/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,20 @@ def problem(status, title, detail, type='about:blank', instance=None, headers=No
Returns a `Problem Details <https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00>`_ error response.
:param type: An absolute URI that identifies the problem type. When dereferenced, it SHOULD provide human-readable
documentation for the problem type (e.g., using HTML). When this member is not present its value is
assumed to be "about:blank".
:type: type: str
:param status: The HTTP status code generated by the origin server for this occurrence of the problem.
:type status: int
:param title: A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to
occurrence of the problem, except for purposes of localisation.
:type title: str
:param detail: An human readable explanation specific to this occurrence of the problem.
:type detail: str
:param status: The HTTP status code generated by the origin server for this occurrence of the problem.
:type status: int
:param type: An absolute URI that identifies the problem type. When dereferenced, it SHOULD provide human-readable
documentation for the problem type (e.g., using HTML). When this member is not present its value is
assumed to be "about:blank".
:type: type: str
:param instance: An absolute URI that identifies the specific occurrence of the problem. It may or may not yield
further information if dereferenced.
:type instance: str
:type type: str | None
:param headers: HTTP headers to include in the response
:type headers: dict | None
:param ext: Extension members to include in the body
Expand Down
38 changes: 38 additions & 0 deletions tests/fakeapi/secure_api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
swagger: "2.0"

info:
title: "{{title}}"
version: "1.0"

basePath: /v1.0

securityDefinitions:
oauth:
type: oauth2
flow: password
tokenUrl: https://ouath.example/token
x-tokenInfoUrl: https://ouath.example/token_info
scopes:
myscope: can do stuff

security:
- oauth:
- myscope

paths:
/greeting/{name}:
post:
summary: Generate greeting
description: Generates a greeting message.
operationId: fakeapi.hello.post_greeting
responses:
200:
description: greeting response
schema:
type: object
parameters:
- name: name
in: path
description: Name of the person to greet.
required: true
type: string
Loading

0 comments on commit 812ed1c

Please sign in to comment.