Skip to content

Commit fde1b6b

Browse files
committed
Security providers and security models retouch
1 parent 4ae5a08 commit fde1b6b

File tree

11 files changed

+131
-64
lines changed

11 files changed

+131
-64
lines changed

openapi_core/schema/security_requirements/generators.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,4 @@ def __init__(self, dereferencer):
1212
def generate(self, security_spec):
1313
security_deref = self.dereferencer.dereference(security_spec)
1414
for security_requirement_spec in security_deref:
15-
name = next(iter(security_requirement_spec))
16-
scope_names = security_requirement_spec[name]
17-
18-
yield SecurityRequirement(name, scope_names=scope_names)
15+
yield SecurityRequirement(security_requirement_spec)
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
"""OpenAPI core security requirements models module"""
22

33

4-
class SecurityRequirement(object):
4+
class SecurityRequirement(dict):
55
"""Represents an OpenAPI Security Requirement."""
6-
7-
def __init__(self, name, scope_names=None):
8-
self.name = name
9-
self.scope_names = scope_names or []
6+
pass

openapi_core/security/__init__.py

Whitespace-only changes.

openapi_core/security/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from openapi_core.exceptions import OpenAPIError
2+
3+
4+
class SecurityError(OpenAPIError):
5+
pass

openapi_core/security/factories.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from openapi_core.schema.security_schemes.enums import SecuritySchemeType
2+
from openapi_core.security.providers import (
3+
ApiKeyProvider, HttpProvider, UnsupportedProvider,
4+
)
5+
6+
7+
class SecurityProviderFactory(object):
8+
9+
PROVIDERS = {
10+
SecuritySchemeType.API_KEY: ApiKeyProvider,
11+
SecuritySchemeType.HTTP: HttpProvider,
12+
}
13+
14+
def create(self, scheme):
15+
if scheme.type == SecuritySchemeType.API_KEY:
16+
return ApiKeyProvider(scheme)
17+
elif scheme.type == SecuritySchemeType.HTTP:
18+
return HttpProvider(scheme)
19+
return UnsupportedProvider(scheme)

openapi_core/security/providers.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import base64
2+
import binascii
3+
import warnings
4+
5+
from openapi_core.security.exceptions import SecurityError
6+
7+
8+
class BaseProvider(object):
9+
10+
def __init__(self, scheme):
11+
self.scheme = scheme
12+
13+
14+
class UnsupportedProvider(BaseProvider):
15+
16+
def __call__(self, request):
17+
warnings.warn("Unsupported scheme type")
18+
19+
20+
class ApiKeyProvider(BaseProvider):
21+
22+
def __call__(self, request):
23+
source = getattr(request.parameters, self.scheme.apikey_in.value)
24+
if self.scheme.name not in source:
25+
raise SecurityError("Missing api key parameter.")
26+
return source.get(self.scheme.name)
27+
28+
29+
class HttpProvider(BaseProvider):
30+
31+
def __call__(self, request):
32+
if 'Authorization' not in request.parameters.header:
33+
raise SecurityError('Missing authorization header.')
34+
auth_header = request.parameters.header['Authorization']
35+
try:
36+
auth_type, encoded_credentials = auth_header.split(' ', 1)
37+
except ValueError:
38+
raise SecurityError('Could not parse authorization header.')
39+
40+
if auth_type.lower() != self.scheme.scheme.value:
41+
raise SecurityError(
42+
'Unknown authorization method %s' % auth_type)
43+
try:
44+
return base64.b64decode(
45+
encoded_credentials.encode('ascii')).decode('latin1')
46+
except binascii.Error:
47+
raise SecurityError('Invalid base64 encoding.')

openapi_core/validation/exceptions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""OpenAPI core validation exceptions module"""
2+
import attr
3+
4+
from openapi_core.exceptions import OpenAPIError
5+
6+
7+
class ValidationError(OpenAPIError):
8+
pass
9+
10+
11+
@attr.s(hash=True)
12+
class InvalidSecurity(ValidationError):
13+
14+
def __str__(self):
15+
return "Security not valid for any requirement"

openapi_core/validation/request/validators.py

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
"""OpenAPI core validation request validators module"""
2-
import base64
3-
import binascii
42
from itertools import chain
53
from six import iteritems
6-
import warnings
74

85
from openapi_core.casting.schemas.exceptions import CastError
96
from openapi_core.deserializing.exceptions import DeserializeError
@@ -14,11 +11,12 @@
1411
)
1512
from openapi_core.schema.paths.exceptions import InvalidPath
1613
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
17-
from openapi_core.schema.security_schemes.enums import SecuritySchemeType
1814
from openapi_core.schema.servers.exceptions import InvalidServer
15+
from openapi_core.security.exceptions import SecurityError
1916
from openapi_core.unmarshalling.schemas.exceptions import (
2017
UnmarshalError, ValidateError,
2118
)
19+
from openapi_core.validation.exceptions import InvalidSecurity
2220
from openapi_core.validation.request.datatypes import (
2321
RequestParameters, RequestValidationResult,
2422
)
@@ -45,8 +43,7 @@ def validate(self, request):
4543

4644
try:
4745
security = self._get_security(request, operation)
48-
# TODO narrow exceptions
49-
except Exception as exc:
46+
except InvalidSecurity as exc:
5047
return RequestValidationResult([exc, ], None, None, None)
5148

5249
params, params_errors = self._get_parameters(
@@ -108,14 +105,16 @@ def _get_security(self, request, operation):
108105
return {}
109106

110107
for security_requirement in security:
111-
data = {
112-
security_requirement.name: self._get_security_value(
113-
security_requirement.name, request)
114-
}
115-
if all(value for value in data.values()):
116-
return data
108+
try:
109+
return {
110+
scheme_name: self._get_security_value(
111+
scheme_name, request)
112+
for scheme_name in security_requirement
113+
}
114+
except SecurityError:
115+
continue
117116

118-
return {}
117+
raise InvalidSecurity()
119118

120119
def _get_parameters(self, request, params):
121120
errors = []
@@ -196,27 +195,10 @@ def _get_security_value(self, scheme_name, request):
196195
if not scheme:
197196
return
198197

199-
if scheme.type == SecuritySchemeType.API_KEY:
200-
source = getattr(request.parameters, scheme.apikey_in.value)
201-
return source.get(scheme.name)
202-
elif scheme.type == SecuritySchemeType.HTTP:
203-
auth_header = request.parameters.header.get('Authorization')
204-
try:
205-
auth_type, encoded_credentials = auth_header.split(' ', 1)
206-
except ValueError:
207-
raise ValueError('Could not parse authorization header.')
208-
209-
if auth_type.lower() != scheme.scheme.value:
210-
raise ValueError(
211-
'Unknown authorization method %s' % auth_type)
212-
try:
213-
return base64.b64decode(
214-
encoded_credentials.encode('ascii'), validate=True
215-
).decode('latin1')
216-
except binascii.Error:
217-
raise ValueError('Invalid base64 encoding.')
218-
219-
warnings.warn("Only api key security scheme type supported")
198+
from openapi_core.security.factories import SecurityProviderFactory
199+
security_provider_factory = SecurityProviderFactory()
200+
security_provider = security_provider_factory.create(scheme)
201+
return security_provider(request)
220202

221203
def _get_parameter_value(self, param, request):
222204
location = request.parameters[param.location.value]

tests/integration/data/v3.0/petstore.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ info:
1313
url: https://opensource.org/licenses/MIT
1414
security:
1515
- api_key: []
16+
- {}
1617
servers:
1718
- url: http://petstore.swagger.io/{version}
1819
variables:

tests/integration/schema/test_spec.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,8 @@ def test_spec(self, spec, spec_dict):
7171
assert type(security_req) == SecurityRequirement
7272

7373
security_req_spec = security_spec[idx]
74-
name = next(iter(security_req_spec))
75-
assert security_req.name == name
76-
assert security_req.scope_names == security_req_spec[name]
74+
for scheme_name in security_req:
75+
security_req[scheme_name] == security_req_spec[scheme_name]
7776

7877
assert spec.get_server_url() == url
7978

@@ -115,15 +114,6 @@ def test_spec(self, spec, spec_dict):
115114
assert ext_docs.description == ext_docs_spec.get(
116115
'description')
117116

118-
security_spec = operation_spec.get('security')
119-
if security_spec:
120-
for idx, sec_req in enumerate(operation.security):
121-
assert type(sec_req) == SecurityRequirement
122-
sec_req_spec = security_spec[idx]
123-
sec_req_nam = next(iter(sec_req_spec))
124-
assert sec_req.name == sec_req_nam
125-
assert sec_req.scope_names == sec_req_spec[sec_req_nam]
126-
127117
servers_spec = operation_spec.get('servers', [])
128118
for idx, server in enumerate(operation.servers):
129119
assert type(server) == Server
@@ -146,9 +136,9 @@ def test_spec(self, spec, spec_dict):
146136
assert type(security_req) == SecurityRequirement
147137

148138
security_req_spec = security_spec[idx]
149-
name = next(iter(security_req_spec))
150-
assert security_req.name == name
151-
assert security_req.scope_names == security_req_spec[name]
139+
for scheme_name in security_req:
140+
security_req[scheme_name] == security_req_spec[
141+
scheme_name]
152142

153143
responses_spec = operation_spec.get('responses')
154144

tests/integration/validation/test_validators.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from openapi_core.shortcuts import create_spec
2121
from openapi_core.testing import MockRequest, MockResponse
2222
from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue
23+
from openapi_core.validation.exceptions import InvalidSecurity
2324
from openapi_core.validation.request.datatypes import RequestParameters
2425
from openapi_core.validation.request.validators import RequestValidator
2526
from openapi_core.validation.response.validators import ResponseValidator
@@ -37,15 +38,15 @@ def api_key_encoded(self):
3738
api_key_bytes_enc = b64encode(api_key_bytes)
3839
return text_type(api_key_bytes_enc, 'utf8')
3940

40-
@pytest.fixture
41+
@pytest.fixture(scope='session')
4142
def spec_dict(self, factory):
4243
return factory.spec_from_file("data/v3.0/petstore.yaml")
4344

44-
@pytest.fixture
45+
@pytest.fixture(scope='session')
4546
def spec(self, spec_dict):
4647
return create_spec(spec_dict)
4748

48-
@pytest.fixture
49+
@pytest.fixture(scope='session')
4950
def validator(self, spec):
5051
return RequestValidator(spec)
5152

@@ -248,6 +249,19 @@ def test_post_pets(self, validator, spec_dict):
248249
assert result.body.address.street == pet_street
249250
assert result.body.address.city == pet_city
250251

252+
def test_get_pet_unauthorized(self, validator):
253+
request = MockRequest(
254+
self.host_url, 'get', '/v1/pets/1',
255+
path_pattern='/v1/pets/{petId}', view_args={'petId': '1'},
256+
)
257+
258+
result = validator.validate(request)
259+
260+
assert result.errors == [InvalidSecurity(), ]
261+
assert result.body is None
262+
assert result.parameters is None
263+
assert result.security is None
264+
251265
def test_get_pet(self, validator):
252266
authorization = 'Basic ' + self.api_key_encoded
253267
headers = {
@@ -275,7 +289,7 @@ def test_get_pet(self, validator):
275289

276290
class TestPathItemParamsValidator(object):
277291

278-
@pytest.fixture
292+
@pytest.fixture(scope='session')
279293
def spec_dict(self):
280294
return {
281295
"openapi": "3.0.0",
@@ -306,11 +320,11 @@ def spec_dict(self):
306320
}
307321
}
308322

309-
@pytest.fixture
323+
@pytest.fixture(scope='session')
310324
def spec(self, spec_dict):
311325
return create_spec(spec_dict)
312326

313-
@pytest.fixture
327+
@pytest.fixture(scope='session')
314328
def validator(self, spec):
315329
return RequestValidator(spec)
316330

0 commit comments

Comments
 (0)