Skip to content

Commit 90bbc55

Browse files
authored
Merge pull request #195 from p1c2u/feature/security-validation
Security validation
2 parents f0759b0 + fde1b6b commit 90bbc55

File tree

24 files changed

+344
-64
lines changed

24 files changed

+344
-64
lines changed

openapi_core/schema/components/factories.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from openapi_core.compat import lru_cache
22
from openapi_core.schema.components.models import Components
33
from openapi_core.schema.schemas.generators import SchemasGenerator
4+
from openapi_core.schema.security_schemes.generators import (
5+
SecuritySchemesGenerator,
6+
)
47

58

69
class ComponentsFactory(object):
@@ -15,15 +18,18 @@ def create(self, components_spec):
1518
schemas_spec = components_deref.get('schemas', {})
1619
responses_spec = components_deref.get('responses', {})
1720
parameters_spec = components_deref.get('parameters', {})
18-
request_bodies_spec = components_deref.get('request_bodies', {})
21+
request_bodies_spec = components_deref.get('requestBodies', {})
22+
security_schemes_spec = components_deref.get('securitySchemes', {})
1923

2024
schemas = self.schemas_generator.generate(schemas_spec)
2125
responses = self._generate_response(responses_spec)
2226
parameters = self._generate_parameters(parameters_spec)
2327
request_bodies = self._generate_request_bodies(request_bodies_spec)
28+
security_schemes = self._generate_security_schemes(
29+
security_schemes_spec)
2430
return Components(
2531
schemas=list(schemas), responses=responses, parameters=parameters,
26-
request_bodies=request_bodies,
32+
request_bodies=request_bodies, security_schemes=security_schemes,
2733
)
2834

2935
@property
@@ -39,3 +45,7 @@ def _generate_parameters(self, parameters_spec):
3945

4046
def _generate_request_bodies(self, request_bodies_spec):
4147
return request_bodies_spec
48+
49+
def _generate_security_schemes(self, security_schemes_spec):
50+
return SecuritySchemesGenerator(self.dereferencer).generate(
51+
security_schemes_spec)

openapi_core/schema/components/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ class Components(object):
33

44
def __init__(
55
self, schemas=None, responses=None, parameters=None,
6-
request_bodies=None):
6+
request_bodies=None, security_schemes=None):
77
self.schemas = schemas and dict(schemas) or {}
88
self.responses = responses and dict(responses) or {}
99
self.parameters = parameters and dict(parameters) or {}
1010
self.request_bodies = request_bodies and dict(request_bodies) or {}
11+
self.security_schemes = (
12+
security_schemes and dict(security_schemes) or {}
13+
)

openapi_core/schema/operations/generators.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from openapi_core.schema.parameters.generators import ParametersGenerator
1212
from openapi_core.schema.request_bodies.factories import RequestBodyFactory
1313
from openapi_core.schema.responses.generators import ResponsesGenerator
14-
from openapi_core.schema.security.factories import SecurityRequirementFactory
14+
from openapi_core.schema.security_requirements.generators import (
15+
SecurityRequirementsGenerator,
16+
)
1517
from openapi_core.schema.servers.generators import ServersGenerator
1618

1719

@@ -39,16 +41,12 @@ def generate(self, path_name, path):
3941
tags_list = operation_deref.get('tags', [])
4042
summary = operation_deref.get('summary')
4143
description = operation_deref.get('description')
42-
security_requirements_list = operation_deref.get('security', [])
44+
security_spec = operation_deref.get('security', [])
4345
servers_spec = operation_deref.get('servers', [])
4446

4547
servers = self.servers_generator.generate(servers_spec)
46-
47-
security = None
48-
if security_requirements_list:
49-
security = list(map(
50-
self.security_requirement_factory.create,
51-
security_requirements_list))
48+
security = self.security_requirements_generator.generate(
49+
security_spec)
5250

5351
external_docs = None
5452
if 'externalDocs' in operation_deref:
@@ -67,10 +65,10 @@ def generate(self, path_name, path):
6765
Operation(
6866
http_method, path_name, responses, list(parameters),
6967
summary=summary, description=description,
70-
external_docs=external_docs, security=security,
68+
external_docs=external_docs, security=list(security),
7169
request_body=request_body, deprecated=deprecated,
7270
operation_id=operation_id, tags=list(tags_list),
73-
servers=servers,
71+
servers=list(servers),
7472
),
7573
)
7674

@@ -96,8 +94,8 @@ def request_body_factory(self):
9694

9795
@property
9896
@lru_cache()
99-
def security_requirement_factory(self):
100-
return SecurityRequirementFactory(self.dereferencer)
97+
def security_requirements_generator(self):
98+
return SecurityRequirementsGenerator(self.dereferencer)
10199

102100
@property
103101
@lru_cache()

openapi_core/schema/security/factories.py

Lines changed: 0 additions & 14 deletions
This file was deleted.

openapi_core/schema/security/models.py

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""OpenAPI core security requirements generators module"""
2+
from openapi_core.schema.security_requirements.models import (
3+
SecurityRequirement,
4+
)
5+
6+
7+
class SecurityRequirementsGenerator(object):
8+
9+
def __init__(self, dereferencer):
10+
self.dereferencer = dereferencer
11+
12+
def generate(self, security_spec):
13+
security_deref = self.dereferencer.dereference(security_spec)
14+
for security_requirement_spec in security_deref:
15+
yield SecurityRequirement(security_requirement_spec)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""OpenAPI core security requirements models module"""
2+
3+
4+
class SecurityRequirement(dict):
5+
"""Represents an OpenAPI Security Requirement."""
6+
pass

openapi_core/schema/security_schemes/__init__.py

Whitespace-only changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""OpenAPI core security schemes enums module"""
2+
from enum import Enum
3+
4+
5+
class SecuritySchemeType(Enum):
6+
7+
API_KEY = 'apiKey'
8+
HTTP = 'http'
9+
OAUTH2 = 'oauth2'
10+
OPEN_ID_CONNECT = 'openIdConnect'
11+
12+
13+
class ApiKeyLocation(Enum):
14+
15+
QUERY = 'query'
16+
HEADER = 'header'
17+
COOKIE = 'cookie'
18+
19+
@classmethod
20+
def has_value(cls, value):
21+
return (any(value == item.value for item in cls))
22+
23+
24+
class HttpAuthScheme(Enum):
25+
26+
BASIC = 'basic'
27+
BEARER = 'bearer'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""OpenAPI core security schemes generators module"""
2+
import logging
3+
4+
from six import iteritems
5+
6+
from openapi_core.schema.security_schemes.models import SecurityScheme
7+
8+
log = logging.getLogger(__name__)
9+
10+
11+
class SecuritySchemesGenerator(object):
12+
13+
def __init__(self, dereferencer):
14+
self.dereferencer = dereferencer
15+
16+
def generate(self, security_schemes_spec):
17+
security_schemes_deref = self.dereferencer.dereference(
18+
security_schemes_spec)
19+
20+
for scheme_name, scheme_spec in iteritems(security_schemes_deref):
21+
scheme_deref = self.dereferencer.dereference(scheme_spec)
22+
scheme_type = scheme_deref['type']
23+
description = scheme_deref.get('description')
24+
name = scheme_deref.get('name')
25+
apikey_in = scheme_deref.get('in')
26+
scheme = scheme_deref.get('scheme')
27+
bearer_format = scheme_deref.get('bearerFormat')
28+
flows = scheme_deref.get('flows')
29+
open_id_connect_url = scheme_deref.get('openIdConnectUrl')
30+
31+
scheme = SecurityScheme(
32+
scheme_type, description=description, name=name,
33+
apikey_in=apikey_in, scheme=scheme,
34+
bearer_format=bearer_format, flows=flows,
35+
open_id_connect_url=open_id_connect_url,
36+
)
37+
yield scheme_name, scheme
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""OpenAPI core security schemes models module"""
2+
from openapi_core.schema.security_schemes.enums import (
3+
SecuritySchemeType, ApiKeyLocation, HttpAuthScheme,
4+
)
5+
6+
7+
class SecurityScheme(object):
8+
"""Represents an OpenAPI Security Scheme."""
9+
10+
def __init__(
11+
self, scheme_type, description=None, name=None, apikey_in=None,
12+
scheme=None, bearer_format=None, flows=None,
13+
open_id_connect_url=None,
14+
):
15+
self.type = SecuritySchemeType(scheme_type)
16+
self.description = description
17+
self.name = name
18+
self.apikey_in = apikey_in and ApiKeyLocation(apikey_in)
19+
self.scheme = scheme and HttpAuthScheme(scheme)
20+
self.bearer_format = bearer_format
21+
self.flows = flows
22+
self.open_id_connect_url = open_id_connect_url

openapi_core/schema/specs/factories.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from openapi_core.schema.infos.factories import InfoFactory
1010
from openapi_core.schema.paths.generators import PathsGenerator
1111
from openapi_core.schema.schemas.registries import SchemaRegistry
12+
from openapi_core.schema.security_requirements.generators import (
13+
SecurityRequirementsGenerator,
14+
)
1215
from openapi_core.schema.servers.generators import ServersGenerator
1316
from openapi_core.schema.specs.models import Spec
1417

@@ -29,6 +32,7 @@ def create(self, spec_dict, spec_url=''):
2932
servers_spec = spec_dict_deref.get('servers', [])
3033
paths = spec_dict_deref.get('paths', {})
3134
components_spec = spec_dict_deref.get('components', {})
35+
security_spec = spec_dict_deref.get('security', [])
3236

3337
if not servers_spec:
3438
servers_spec = [
@@ -39,8 +43,13 @@ def create(self, spec_dict, spec_url=''):
3943
servers = self.servers_generator.generate(servers_spec)
4044
paths = self.paths_generator.generate(paths)
4145
components = self.components_factory.create(components_spec)
46+
47+
security = self.security_requirements_generator.generate(
48+
security_spec)
49+
4250
spec = Spec(
4351
info, list(paths), servers=list(servers), components=components,
52+
security=list(security),
4453
_resolver=self.spec_resolver,
4554
)
4655
return spec
@@ -74,3 +83,8 @@ def paths_generator(self):
7483
@lru_cache()
7584
def components_factory(self):
7685
return ComponentsFactory(self.dereferencer, self.schemas_registry)
86+
87+
@property
88+
@lru_cache()
89+
def security_requirements_generator(self):
90+
return SecurityRequirementsGenerator(self.dereferencer)

openapi_core/schema/specs/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ class Spec(object):
1515
"""Represents an OpenAPI Specification for a service."""
1616

1717
def __init__(
18-
self, info, paths, servers=None, components=None, _resolver=None):
18+
self, info, paths, servers=None, components=None,
19+
security=None, _resolver=None):
1920
self.info = info
2021
self.paths = paths and dict(paths)
2122
self.servers = servers or []
2223
self.components = components
24+
self.security = security
2325

2426
self._resolver = _resolver
2527

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/datatypes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,4 @@ def full_url_pattern(self):
7171
class RequestValidationResult(BaseValidationResult):
7272
body = attr.ib(default=None)
7373
parameters = attr.ib(factory=RequestParameters)
74+
security = attr.ib(default=None)

0 commit comments

Comments
 (0)