Skip to content

Commit

Permalink
Added support for checking security prescence in the Swagger plugin.
Browse files Browse the repository at this point in the history
Today this only support the apiKey type, mostly because Bravado
lacks support for anything else, which is extremely unfortunate.

This commit will also bump us to 2.0.10.
  • Loading branch information
rpcope1 committed Dec 12, 2020
1 parent 01774a0 commit 64ab37c
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 7 deletions.
4 changes: 4 additions & 0 deletions README.rst
Expand Up @@ -76,12 +76,16 @@ There are a number of arguments that you can pass to the plugin constructor:

* ``ignore_undefined_api_routes`` - Boolean (default ``False``) Should any routes under the given base path that don't have a Swagger route automatically trigger a 404?

* ``ignore_security_definitions`` - Boolean (default ``False``) Should we ignore the security requirements specified in the swagger spec? This allows you to use things like Cookie auth as an undocumented fallback without Bravado complaining.

* ``auto_jsonify`` - Boolean (default ``False``) If the Swagger route handlers return a list or dict, should we attempt to automatically convert them to a JSON response?

* ``invalid_request_handler`` - Callback called when request validation has failed. Default behaviour is to return a "400 Bad Request" response.

* ``invalid_response_handler`` - Callback called when response validation has failed. Default behaviour is to return a "500 Server Error" response.

* ``invalid_security_handler`` -- (Exception -> HTTP Response) This handler is triggered when no valid forms of authentication matching the Swagger spec were in the incoming request. This is ignored if ``ignore_security_definitions`` is set to True.

* ``swagger_op_not_found_handler`` - Callback called when no swagger operation matching the request was found in the swagger schema. Default behaviour is to return a "404 Not Found" response.

* ``exception_handler=_server_error_handler`` - Callback called when an exception is thrown by downstream handlers (including exceptions thrown by your code). Default behaviour is to return a "500 Server Error" response.
Expand Down
73 changes: 69 additions & 4 deletions bottle_swagger/__init__.py
@@ -1,10 +1,10 @@
__version__ = (2, 0, 9)
__version__ = (2, 0, 10)

import os
import re
import logging
from bottle import request, response, HTTPResponse, json_dumps, static_file
from bravado_core.exception import MatchingResponseNotFound
from bravado_core.exception import MatchingResponseNotFound, SwaggerSecurityValidationError
from bravado_core.request import IncomingRequest, unmarshal_request
from bravado_core.response import OutgoingResponse, validate_response, get_response_spec
from bravado_core.spec import Spec
Expand Down Expand Up @@ -75,6 +75,25 @@ def default_bad_request_handler(e):
return _error_response(400, e)


def default_invalid_security_handler(e):
"""
The default error handler function for lack of auth
failures.
Returns a JSON payload of
{"code": 401, "message": str(e)}
And sets the status code to 401.
:param e: The exception that was thrown Bravado Core upon request validation failure.
:type e: BaseException
:return: The response payload.
:rtype: dict
"""
return _error_response(401, e)


def default_not_found_handler(r):
"""
The default error handler function for route not found failures.
Expand All @@ -93,6 +112,31 @@ def default_not_found_handler(r):
return _error_response(404, r)


class SecurityPatchedOperation(object):
def __init__(self, core_op):
self._core_op = core_op

def __repr__(self):
return repr(self._core_op)

@property
def security_specs(self):
return []

@property
def security_requirements(self):
return []

def __getattr__(self, item):
return getattr(self._core_op, item)

def __setattr__(self, key, value):
if key == "_core_op":
self.__dict__[key] = value
else:
return setattr(self._core_op, key, value)


class SwaggerPlugin(object):
"""
This plugin allows the user to use Swagger 2.0 and Bravado Core to write a REST API with validation
Expand Down Expand Up @@ -121,12 +165,17 @@ class SwaggerPlugin(object):
* ``internally_dereference_refs`` -- (bool) Should Bravado fully derefence $refs (for a performance speed up)?
* ``ignore_undefined_api_routes`` -- (bool) Should we ignore undefined API routes, and trigger the
swagger_op_not_found handler?
* ``ignore_security_definitions`` -- (bool) Should we ignore the security requirements specified in the swagger
spec? This allows you to use things like Cookie auth as an undocumented fallback without Bravado complaining.
* ``auto_jsonify`` -- (bool) Should we automatically convert data returned from our callbacks to JSON? Bottle
normally will attempt to convert only objects, but we can do better.
* ``invalid_request_handler`` -- (Exception -> HTTP Response) This handler is triggered when the
request validation fails.
* ``invalid_response_handler`` -- (Exception -> HTTP Response) This handler is triggered when
the response validation fails.
* ``invalid_security_handler`` -- (Exception -> HTTP Response) This handler is triggered when
no valid forms of authentication matching the Swagger spec were in the incoming request. This is
ignored if ``ignore_security_definitions`` is set to True.
* ``swagger_op_not_found_handler`` -- (bottle.Route -> HTTP Response) This handler is triggered if the
route isn't found for the API subpath, and ignore_missing_routes has been set True.
* ``exception_handler`` -- (Base Exception -> HTTP Response.) This handler is triggered if the
Expand Down Expand Up @@ -157,9 +206,11 @@ def __init__(self, swagger_def,
default_type_to_object=False,
internally_dereference_refs=False,
ignore_undefined_api_routes=False,
ignore_security_definitions=False,
auto_jsonify=True,
invalid_request_handler=default_bad_request_handler,
invalid_response_handler=default_server_error_handler,
invalid_security_handler=default_invalid_security_handler,
swagger_op_not_found_handler=default_not_found_handler,
exception_handler=default_server_error_handler,
swagger_base_path=None,
Expand Down Expand Up @@ -196,13 +247,19 @@ def __init__(self, swagger_def,
:param ignore_undefined_api_routes: Should we ignore undefined API routes, and trigger the
swagger_op_not_found handler?
:type ignore_undefined_api_routes: bool
:param ignore_security_definitions: Should we ignore the set security definitions? This might make sense if
you also want to permit Cookie auth (which is not available in OpenAPI 2).
:type ignore_security_definitions: bool
:param auto_jsonify: Should we automatically convert data returned from our callbacks to JSON? Bottle
normally will attempt to convert only objects, but we can do better.
:type auto_jsonify: bool
:param invalid_request_handler: This handler is triggered when the request validation fails.
:type invalid_request_handler: BaseException -> HTTP Response
:param invalid_response_handler: This handler is triggered when the response validation fails.
:type invalid_response_handler: BaseException -> HTTP Response
:param invalid_security_handler: This handler is triggered when no means of authentication
were found for the request.
:type invalid_security_handler: BaseException -> HTTP Response
:param swagger_op_not_found_handler: This handler is triggered if the route isn't found for the API subpath,
and ignore_missing_routes has been set True.
:type swagger_op_not_found_handler: bottle.Route -> HTTP Response
Expand Down Expand Up @@ -238,9 +295,11 @@ def __init__(self, swagger_def,
swagger_def.update(basePath=swagger_base_path)

self.ignore_undefined_routes = ignore_undefined_api_routes
self.ignore_security_definitions = ignore_security_definitions
self.auto_jsonify = auto_jsonify
self.invalid_request_handler = invalid_request_handler
self.invalid_response_handler = invalid_response_handler
self.invalid_security_handler = invalid_security_handler
self.swagger_op_not_found_handler = swagger_op_not_found_handler
self.exception_handler = exception_handler
self.serve_swagger_ui = serve_swagger_ui
Expand Down Expand Up @@ -340,7 +399,11 @@ def _swagger_validate(self, callback, route, *args, **kwargs):
request.swagger_op = swagger_op

try:
request.swagger_data = self._validate_request(swagger_op)
request.swagger_data = self._validate_request(
swagger_op, ignore_security_definitions=self.ignore_security_definitions
)
except SwaggerSecurityValidationError as e:
return self.invalid_security_handler(e)
except ValidationError as e:
return self.invalid_request_handler(e)

Expand Down Expand Up @@ -368,7 +431,9 @@ def _swagger_validate(self, callback, route, *args, **kwargs):
return result

@staticmethod
def _validate_request(swagger_op):
def _validate_request(swagger_op, ignore_security_definitions=False):
if ignore_security_definitions:
swagger_op = SecurityPatchedOperation(swagger_op)
return unmarshal_request(BottleIncomingRequest(request), swagger_op)

@staticmethod
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Expand Up @@ -11,7 +11,7 @@ def _read(fname):


REQUIREMENTS = [l for l in _read('requirements.txt').split('\n') if l and not l.startswith('#')]
VERSION = '2.0.9'
VERSION = '2.0.10'

setup(
name='bottle-swagger-2',
Expand All @@ -37,7 +37,6 @@ def _read(fname):
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
Expand Down
120 changes: 119 additions & 1 deletion test_bottle_swagger/test_bottle_swagger.py
@@ -1,6 +1,6 @@
from unittest import TestCase

from bottle import Bottle, redirect, request, HTTPResponse
from bottle import Bottle, redirect, request, HTTPResponse, debug
from bottle_swagger import SwaggerPlugin
from webtest import TestApp

Expand All @@ -9,6 +9,56 @@ class TestBottleSwagger(TestCase):
VALID_JSON = {"id": "123", "name": "foo"}
INVALID_JSON = {"not_id": "123", "name": "foo"}

SWAGGER_DEF_WITH_SECURITY = {
"swagger": "2.0",
"info": {"version": "1.0.0", "title": "bottle-swagger"},
"consumes": ["application/json"],
"produces": ["application/json"],
"definitions": {
"Thing": {
"type": "object",
}
},
"securityDefinitions": {
"BasicAuth": {
"type": "basic"
},
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
'name': "X-API-Key"
}
},
"paths": {
"/thing": {
"get": {
"security": [{"BasicAuth": []}],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/Thing"
}
}
}
}
},
"/thing2": {
"get": {
"security": [{"ApiKeyAuth": []}],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/Thing"
}
}
}
}
}
}
}

SWAGGER_DEF = {
"swagger": "2.0",
"info": {"version": "1.0.0", "title": "bottle-swagger"},
Expand Down Expand Up @@ -404,6 +454,71 @@ def index():
assert "http://bar.com/wat.json" not in response
assert "http://test.com/test" not in response

def test_security_spec_functionality(self):
bottle_app = Bottle()
bottle_app.install(self._make_security_swagger_plugin())

@bottle_app.route("/thing", "GET")
def index():
assert request.auth is not None, "BasicAuth must be set"
return {}

@bottle_app.route("/thing2", "GET")
def bar():
assert 'X-API-Key' in request.swagger_data, 'API Key auth must be set'
return {}

test_app = TestApp(bottle_app)
old_auth = None
test_app.authorization = old_auth
resp = test_app.get("/thing2", headers={"X-API-Key": "foobar"})
assert resp.status_code == 200, resp.status + "\n" + resp.body.decode('utf-8')

test_app.authorization = old_auth
resp = test_app.get("/thing2", expect_errors=True)
assert resp.status_code == 401, resp.status + "\n" + resp.body.decode('utf-8')

test_app.authorization = ("Basic", ("foo", "bar"))
resp = test_app.get("/thing2", expect_errors=True)
assert resp.status_code == 401, resp.status + "\n" + resp.body.decode('utf-8')

test_app.authorization = ("Basic", ("foo", "bar"))
resp = test_app.get("/thing")
assert resp.status_code == 200, resp.status + "\n" + resp.body.decode('utf-8')

# Bravado does not support basic auth validation upstream ...
# test_app.authorization = old_auth
# resp = test_app.get("/thing", expect_errors=True)
# assert resp.status_code == 401, resp.status + "\n" + resp.body.decode('utf-8')

# test_app.authorization = old_auth
# resp = test_app.get("/thing", headers={"X-API-Key": "foobar"}, expect_errors=True)
# assert resp.status_code == 401, resp.status + "\n" + resp.body.decode('utf-8')

def test_security_spec_ignored(self):
bottle_app = Bottle()
debug()
bottle_app.install(self._make_security_swagger_plugin(
ignore_security_definitions=True
))

@bottle_app.route("/thing", "GET")
def index():
return {}

@bottle_app.route("/thing2", "GET")
def bar():
return {}

test_app = TestApp(bottle_app)
test_app.authorization = None
resp = test_app.get("/thing")
assert resp.status_code == 200

test_app.authorization = None
resp = test_app.get("/thing2")
assert resp.status_code == 200

def _test_request(self, swagger_plugin=None, method='GET', url='/thing', route_url=None, request_json=VALID_JSON,
response_json=VALID_JSON, headers=None, content_type='application/json',
extra_check=lambda *args, **kwargs: True):
Expand Down Expand Up @@ -465,3 +580,6 @@ def _assert_error_response(self, response, expected_status):

def _make_swagger_plugin(self, *args, **kwargs):
return SwaggerPlugin(self.SWAGGER_DEF, *args, **kwargs)

def _make_security_swagger_plugin(self, *args, **kwargs):
return SwaggerPlugin(self.SWAGGER_DEF_WITH_SECURITY, *args, **kwargs)

0 comments on commit 64ab37c

Please sign in to comment.