Skip to content

Commit

Permalink
Merge pull request #112 from macisamuele/security-objects-handling
Browse files Browse the repository at this point in the history
Security Objects handling and parameter validation
  • Loading branch information
sjaensch committed Aug 25, 2016
2 parents 3f5b09a + 31e4ad2 commit 3d469f5
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 18 deletions.
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Expand Up @@ -3,12 +3,13 @@
sha: 5fe82b
hooks:
- id: autopep8-wrapper
args: ['-i', '--ignore=E309']
args: ['-i', '--ignore=E309,E501']
- id: check-json
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: flake8
args: [--max-line-length=131]
- id: name-tests-test
- id: trailing-whitespace
- id: requirements-txt-fixer
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Expand Up @@ -19,6 +19,11 @@ test:

tests: test


.PHONY: install-hooks
install-hooks:
tox -e pre-commit -- install -f --install-hooks

clean:
@rm -rf .tox build dist docs/build *.egg-info
find . -name '*.pyc' -delete
Expand Down
72 changes: 65 additions & 7 deletions bravado_core/operation.py
Expand Up @@ -2,20 +2,22 @@
import logging
import re

from bravado_core.exception import SwaggerSchemaError
from bravado_core.param import Param

log = logging.getLogger(__name__)


class Operation(object):
"""Swagger operation defined by a unique (http_method, path_name) pair.

:type swagger_spec: :class:`Spec`
:param path_name: path of the operation. e.g. /pet/{petId}
:param http_method: get/put/post/delete/etc
:param op_spec: operation specification in dict form
"""
def __init__(self, swagger_spec, path_name, http_method, op_spec):
"""Swagger operation defined by a unique (http_method, path_name) pair.
:type swagger_spec: :class:`Spec`
:param path_name: path of the operation. e.g. /pet/{petId}
:param http_method: get/put/post/delete/etc
:param op_spec: operation specification in dict form
"""
self.swagger_spec = swagger_spec
self.path_name = path_name
self.http_method = http_method
Expand All @@ -26,6 +28,9 @@ def __init__(self, swagger_spec, path_name, http_method, op_spec):
# in the Swagger 2.0 Spec.
self._operation_id = None

# generated by @property to avoid multiple swagger validations
self._security_objects = None

# (key, value) = (param name, Param)
self.params = {}

Expand All @@ -44,6 +49,36 @@ def consumes(self):
result = deref(self.swagger_spec.spec_dict.get('consumes', []))
return result

@property
def security_objects(self):
"""Builds up the list of security options for this operation
:returns: list of security definition associated to the operation
"""
if self._security_objects is None:
deref = self.swagger_spec.deref
op_spec = deref(self.op_spec)
spec_dict = deref(self.swagger_spec.spec_dict)
security_spec = deref(op_spec.get('security', []))
if len(security_spec) == 0:
security_spec = spec_dict.get('security', [])
security_defs_dict = spec_dict.get('securityDefinitions', {})

self._security_objects = []
for security_item in security_spec:
for security_name, security_scope in security_item.items():
security_definition = security_defs_dict.get(security_name)
if security_definition is None:
raise SwaggerSchemaError(
'{security} not defined in {swagger_path}'.format(
swagger_path='/securityDefinitions',
security=security_name,
)
)
self._security_objects.append(security_definition)

return self._security_objects

@property
def produces(self):
"""Note that the operation can override the value defined globally
Expand Down Expand Up @@ -111,6 +146,28 @@ def __repr__(self):
return u"%s(%s)" % (self.__class__.__name__, self.operation_id)


def _build_params_from_security_objects(op):
"""
Generate the required parameters from the security object definition.
NOTE: the current implementation handles only basic and apiKey types.
:type op: :class:`bravado_core.operation.Operation`
:return: list of well formatted parameters (JSON-like notation)
:rtype: list
"""
security_params_spec = []
for security_definition in op.security_objects:
if security_definition['type'] == 'apiKey':
security_params_spec.append({
'required': True,
'type': 'string',
'description': security_definition.get('description', ''),
'name': security_definition['name'],
'in': security_definition['in']
})
return security_params_spec


def build_params(op):
"""Builds up the list of this operation's parameters taking into account
parameters that may be available for this operation's path component.
Expand All @@ -127,11 +184,12 @@ def build_params(op):
paths_spec = deref(spec_dict.get('paths', {}))
path_spec = deref(paths_spec.get(op.path_name))
path_params_spec = deref(path_spec.get('parameters', []))
security_params_spec = _build_params_from_security_objects(op)

# Order of addition is *important* here. Since op_params are last in the
# list, they will replace any previously defined path_params with the
# same name when the final params dict is constructed in the loop below.
params_spec = path_params_spec + op_params_spec
params_spec = path_params_spec + security_params_spec + op_params_spec

params = {}
for param_spec in params_spec:
Expand Down
148 changes: 148 additions & 0 deletions tests/operation/conftest.py
@@ -0,0 +1,148 @@
import pytest
from six import iteritems

SECURITY_DEFINITIONS = {
'basic': {
'basic': {
'type': 'basic',
},
},
'apiKey': {
'apiKey': {
'type': 'apiKey',
'name': 'api_key',
'in': 'header',
},
},
'oauth2': {
'oauth2': {
'type': 'oauth2',
'authorizationUrl': 'http://petstore.swagger.io/api/oauth/dialog',
'flow': 'implicit',
'scopes': {
'write:pets': 'modify pets in your account',
'read:pets': 'read your pets',
},
},
}
}
SECURITY_OBJECTS = {
'basic': [{'basic': []}],
'apiKey': [{'apiKey': []}],
'oauth2': [{'oauth2': []}],
}


def test_security_object_and_definition_constants():
assert SECURITY_OBJECTS.keys() == SECURITY_DEFINITIONS.keys()


@pytest.fixture
def definitions_spec():
return {
'Pet': {
'type': 'object',
'required': ['name'],
'properties': {
'name': {'type': 'string'},
'age': {'type': 'integer'},
'breed': {'type': 'string'}
}
}
}


@pytest.fixture
def _paths_spec():
# The '#/paths' dict from a spec
return {
'/pet/findByStatus': {
'get': {
'tags': [
'pet'
],
'summary': 'Finds Pets by status',
'description': 'Multiple status values can be provided with comma seperated strings', # noqa
'operationId': 'findPetsByStatus',
'produces': [
'application/json',
'application/xml'
],
'parameters': [
{
'name': 'status',
'in': 'query',
'description': 'Status values that need to be considered for filter', # noqa
'required': False,
'type': 'array',
'items': {
'type': 'string'
},
'collectionFormat': 'multi',
'default': 'available'
}
],
'responses': {
'200': {
'description': 'successful operation',
'schema': {
'type': 'array',
'items': {
'$ref': '#/definitions/Pet'
}
}
},
'400': {
'description': 'Invalid status value'
}
},
}
},
}


@pytest.fixture(
params=SECURITY_OBJECTS.keys(),
)
def specs_with_security_obj_in_op_and_security_specs(request, _paths_spec,
definitions_spec):
security_object = SECURITY_OBJECTS[request.param]

for path, path_item in iteritems(_paths_spec):
for http_method in path_item.keys():
path_item[http_method]['security'] = security_object

return {
'paths': _paths_spec,
'definitions': definitions_spec,
'securityDefinitions': SECURITY_DEFINITIONS[request.param],
}


@pytest.fixture
def specs_with_security_obj_in_op_and_no_security_specs(
specs_with_security_obj_in_op_and_security_specs
):
del specs_with_security_obj_in_op_and_security_specs['securityDefinitions']
return specs_with_security_obj_in_op_and_security_specs


@pytest.fixture(
params=SECURITY_OBJECTS.keys(),
)
def specs_with_security_obj_in_root_and_security_specs(request, _paths_spec,
definitions_spec):
return {
'paths': _paths_spec,
'definitions': definitions_spec,
'security': SECURITY_OBJECTS[request.param],
'securityDefinitions': SECURITY_DEFINITIONS[request.param],
}


@pytest.fixture
def specs_with_security_obj_in_root_and_no_security_specs(
specs_with_security_obj_in_root_and_security_specs
):
del specs_with_security_obj_in_root_and_security_specs['securityDefinitions'] # noqa
return specs_with_security_obj_in_root_and_security_specs
88 changes: 88 additions & 0 deletions tests/operation/security_object_test.py
@@ -0,0 +1,88 @@
from mock import Mock
import pytest
from six import iteritems

from bravado_core.exception import SwaggerSchemaError
from bravado_core.request import IncomingRequest, unmarshal_request
from bravado_core.resource import build_resources
from bravado_core.spec import Spec
from jsonschema import ValidationError


def test_op_with_security_in_op_without_security_defs(
specs_with_security_obj_in_op_and_no_security_specs,
):
with pytest.raises(SwaggerSchemaError):
build_resources(Spec(
specs_with_security_obj_in_op_and_no_security_specs,
))


def test_op_with_security_in_root_without_security_defs(
specs_with_security_obj_in_root_and_no_security_specs,
):
with pytest.raises(SwaggerSchemaError):
build_resources(Spec(
specs_with_security_obj_in_root_and_no_security_specs,
))


def _validate_resources(resources, security_definitions_spec):
resource = resources.get('pet')
assert resource is not None
for security_option, security_obj in iteritems(security_definitions_spec):
operation = getattr(resource, 'findPetsByStatus')
assert operation is not None
assert security_obj in operation.security_objects
if security_option == 'apiKey':
assert len(operation.params) == 2
assert security_obj['name'] in operation.params
else:
assert len(operation.params) == 1


def test_op_with_security_in_op_with_security_defs(
specs_with_security_obj_in_op_and_security_specs,
):
security_definitions_spec = \
specs_with_security_obj_in_op_and_security_specs['securityDefinitions']
_validate_resources(
resources=build_resources(Spec(
specs_with_security_obj_in_op_and_security_specs,
)),
security_definitions_spec=security_definitions_spec,
)


def test_op_with_security_in_root_with_security_defs(
specs_with_security_obj_in_root_and_security_specs,
):
security_definitions_spec = \
specs_with_security_obj_in_root_and_security_specs['securityDefinitions'] # noqa
_validate_resources(
resources=build_resources(Spec(
specs_with_security_obj_in_root_and_security_specs,
)),
security_definitions_spec=security_definitions_spec,
)


def test_correct_request_with_apiKey_security(petstore_spec):
request = Mock(
spec=IncomingRequest,
path={'petId': '1234'},
headers={'api_key': 'key1'},
)
op = petstore_spec.resources['pet'].operations['getPetById']
unmarshal_request(request, op)


def test_wrong_request_with_apiKey_security(petstore_spec):
request = Mock(
spec=IncomingRequest,
path={'petId': '1234'},
headers={},
)
op = petstore_spec.resources['pet'].operations['getPetById']
with pytest.raises(ValidationError):
unmarshal_request(request, op)

0 comments on commit 3d469f5

Please sign in to comment.