Skip to content

Commit cfb1dad

Browse files
authored
auth improvements: warn if not known (#60)
* clean up parser a bit * fix ci settings again * remove unneeded security scheme wrapper * clean up security schemes a bit * move security scheme detection into detector * add warning for unsupported auth
1 parent fbae122 commit cfb1dad

File tree

13 files changed

+191
-188
lines changed

13 files changed

+191
-188
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ on:
55
push:
66
branches:
77
- master
8+
- devel
89
pull_request:
910
branches:
1011
- master
12+
- devel
1113
workflow_dispatch:
1214

1315
concurrency:

dlt_openapi/__init__.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from pathlib import Path
66
from typing import Optional, cast
77

8+
import httpcore
9+
import httpx
810
from loguru import logger
911

1012
from dlt_openapi.utils.misc import import_class_from_string
@@ -31,18 +33,20 @@ class Project: # pylint: disable=too-many-instance-attributes
3133
def __init__(
3234
self,
3335
*,
36+
doc: bytes,
3437
openapi: OpenapiParser,
3538
detector: BaseDetector,
3639
renderer: BaseRenderer,
3740
config: Config,
3841
) -> None:
42+
self.doc = doc
3943
self.openapi = openapi
4044
self.detector = detector
4145
self.renderer = renderer
4246
self.config = config
4347

4448
def parse(self) -> None:
45-
self.openapi.parse()
49+
self.openapi.parse(self.doc)
4650

4751
def detect(self) -> None:
4852
logger.info("Running heuristics on parsed output")
@@ -80,16 +84,37 @@ def print_warnings(self) -> None:
8084
logger.warning(w.msg)
8185

8286

87+
def _get_document(*, url: Optional[str] = None, path: Optional[Path] = None, timeout: int = 60) -> bytes:
88+
if url is not None and path is not None:
89+
raise ValueError("Provide URL or Path, not both.")
90+
if url is not None:
91+
logger.info(f"Downloading spec from {url}")
92+
try:
93+
response = httpx.get(url, timeout=timeout)
94+
logger.success("Download complete")
95+
return response.content
96+
except (httpx.HTTPError, httpcore.NetworkError) as e:
97+
raise ValueError("Could not get OpenAPI document from provided URL") from e
98+
elif path is not None:
99+
logger.info(f"Reading spec from {path}")
100+
return Path(path).read_bytes()
101+
else:
102+
raise ValueError("No URL or Path provided")
103+
104+
83105
def _get_project_for_url_or_path( # pylint: disable=too-many-arguments
84106
url: Optional[str],
85107
path: Optional[Path],
86108
config: Config = None,
87109
) -> Project:
110+
doc = _get_document(url=url, path=path)
111+
88112
renderer_cls = cast(BaseRenderer, import_class_from_string(config.renderer_class))
89113
detector_cls = cast(BaseDetector, import_class_from_string(config.detector_class))
90114

91115
return Project(
92-
openapi=OpenapiParser(config, url or path),
116+
doc=doc,
117+
openapi=OpenapiParser(config),
93118
detector=detector_cls(config), # type: ignore
94119
renderer=renderer_cls(config), # type: ignore
95120
config=config,

dlt_openapi/detector/default/__init__.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
DataResponseUndetectedWarning,
4343
PrimaryKeyNotFoundWarning,
4444
UnresolvedPathParametersWarning,
45+
UnsupportedSecuritySchemeWarning,
4546
)
4647

4748
Tree = Dict[str, Union["str", "Tree"]]
@@ -58,6 +59,9 @@ def run(self, open_api: OpenapiParser) -> None:
5859
"""Run the detector"""
5960
self.warnings = {}
6061

62+
# detect security stuff
63+
self.detect_security_schemes(open_api)
64+
6165
# discover stuff from responses
6266
self.detect_paginators_and_responses(open_api.endpoints)
6367

@@ -78,6 +82,40 @@ def run(self, open_api: OpenapiParser) -> None:
7882
if params := e.unresolvable_path_param_names:
7983
self._add_warning(UnresolvedPathParametersWarning(params), e)
8084

85+
def detect_security_schemes(self, open_api: OpenapiParser) -> None:
86+
schemes = list(open_api.security_schemes.values())
87+
88+
# detect scheme settings
89+
for scheme in schemes:
90+
91+
if scheme.type == "apiKey":
92+
scheme.detected_secret_name = "api_key"
93+
scheme.detected_auth_vars = f"""
94+
"type": "api_key",
95+
"api_key": api_key,
96+
"name": "{scheme.name}",
97+
"location": "{scheme.location}"
98+
"""
99+
elif scheme.type == "http" and scheme.scheme == "basic":
100+
scheme.detected_secret_name = "password"
101+
scheme.detected_auth_vars = """
102+
"type": "http_basic",
103+
"username": "username",
104+
"password": password,
105+
"""
106+
elif scheme.type == "http" and scheme.scheme == "bearer":
107+
scheme.detected_secret_name = "token"
108+
scheme.detected_auth_vars = """
109+
"type": "bearer",
110+
"token": token,
111+
"""
112+
113+
# find default scheme
114+
if len(schemes) and schemes[0].supported:
115+
open_api.detected_default_security_scheme = schemes[0]
116+
elif len(schemes) and not schemes[0].supported:
117+
self._add_warning(UnsupportedSecuritySchemeWarning(schemes[0].name))
118+
81119
def detect_resource_names(self, endpoints: EndpointCollection) -> None:
82120
"""iterate all endpoints and find a strategy to select the right resource name"""
83121

dlt_openapi/detector/default/warnings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,12 @@ class DataResponseNoBodyWarning(BaseDetectionWarning):
2626
"No json response schema defined on main data response. "
2727
+ "Will not be able to detect primary key and some paginators."
2828
)
29+
30+
31+
class UnsupportedSecuritySchemeWarning(BaseDetectionWarning):
32+
def __init__(self, security_scheme: str) -> None:
33+
self.security_scheme = security_scheme
34+
self.msg = (
35+
f"Security Scheme {security_scheme} is not supported natively at this time. "
36+
+ "Please provide a custom implementation."
37+
)

dlt_openapi/parser/context.py

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from dataclasses import dataclass
21
from typing import Any, Dict, Optional, Tuple, Union
32

43
import openapi_schema_pydantic as osp
54
import referencing
65
import referencing.jsonschema
76

87
from dlt_openapi.parser.config import Config
9-
from dlt_openapi.utils.misc import ClassName
108

119
TComponentClass = Union[
1210
osp.Schema,
@@ -20,41 +18,19 @@
2018
]
2119

2220

23-
@dataclass
24-
class SecurityScheme:
25-
os_security_scheme: osp.SecurityScheme
26-
class_name: ClassName
27-
28-
@property
29-
def type(self) -> str:
30-
return self.os_security_scheme.type
31-
32-
@property
33-
def scheme(self) -> str:
34-
return self.os_security_scheme.scheme
35-
36-
@property
37-
def name(self) -> str:
38-
return self.os_security_scheme.name
39-
40-
@property
41-
def location(self) -> str:
42-
return self.os_security_scheme.security_scheme_in
43-
44-
4521
class OpenapiContext:
4622
spec: osp.OpenAPI
4723
spec_raw: Dict[str, Any]
24+
config: Config
4825

49-
_component_cache: Dict[str, Dict[str, Any]]
50-
security_schemes: Dict[str, SecurityScheme]
26+
_component_cache: Dict[str, Dict[str, Any]] = {}
5127

5228
def __init__(self, config: Config, spec: osp.OpenAPI, spec_raw: Dict[str, Any]) -> None:
5329
self.config = config
5430
self.spec = spec
5531
self.spec_raw = spec_raw
56-
self._component_cache = {}
57-
self.security_schemes = {}
32+
33+
# setup ref resolver
5834
resource = referencing.Resource( # type: ignore[var-annotated, call-arg]
5935
contents=self.spec_raw, specification=referencing.jsonschema.DRAFT202012
6036
)
@@ -99,8 +75,3 @@ def parameter_from_reference(self, ref: Union[osp.Reference, osp.Parameter]) ->
9975
if isinstance(ref, osp.Parameter):
10076
return ref
10177
return osp.Parameter.parse_obj(self._component_from_reference(ref))
102-
103-
def get_security_scheme(self, name: str) -> SecurityScheme:
104-
if name in self.security_schemes:
105-
return self.security_schemes[name]
106-
return None

dlt_openapi/parser/credentials.py

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

0 commit comments

Comments
 (0)