Skip to content

Commit e8e115c

Browse files
committed
refactor auth: delegate YAML loading and IRI matching to oold.backend.auth
- Replace inline YAML parsing in get_credential() with oold.backend.auth.load_credentials - Replace inline IRI matching with oold.backend.auth.find_credential - Add _oold_to_osw() converter for SecretStr to plain str credential types - Keep OswBaseModel base class (required by WtSiteConfig v1 field) - Keep osw-specific features: .gitignore auto-update, default_paths - Backward compatible: all existing tests pass unchanged
1 parent 83859f1 commit e8e115c

2 files changed

Lines changed: 89 additions & 97 deletions

File tree

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ python_requires = >=3.10
4848
# new major versions. This works if the required packages follow Semantic Versioning.
4949
# For more information, check out https://semver.org/.
5050
install_requires =
51-
oold>=0.11.1
51+
oold>=0.15.0
5252
opensemantic
5353
opensemantic.core>=0.57.4
5454
opensemantic.base>=0.42.7

src/osw/auth.py

Lines changed: 88 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,39 @@
33
import getpass
44
from enum import Enum
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, List, Optional, Union
7-
from warnings import warn
6+
from typing import List, Optional, Union
87

98
import yaml
9+
from oold.backend.auth import UserPwdCredential as _OoldUserPwdCredential
10+
from oold.backend.auth import find_credential as _find_credential
11+
from oold.backend.auth import load_credentials as _load_credentials
1012
from opensemantic.v1 import OswBaseModel
1113
from pydantic.v1 import PrivateAttr
1214

1315
from osw.defaults import paths as default_paths
1416

15-
if TYPE_CHECKING:
16-
PossibleFilePath = Path
17-
else:
18-
from osw.custom_types import PossibleFilePath
17+
18+
def _secret_to_str(v):
19+
"""Unwrap SecretStr to plain str, pass through otherwise."""
20+
if hasattr(v, "get_secret_value"):
21+
return v.get_secret_value()
22+
return v
1923

2024

2125
class CredentialManager(OswBaseModel):
22-
"""Handles credentials"""
26+
"""Handles credentials.
27+
28+
Delegates YAML loading and IRI matching to oold.backend.auth,
29+
adding osw-specific features (default paths, .gitignore management).
30+
Remains a v1 model because WtSiteConfig (v1) uses it as a field.
31+
"""
2332

24-
cred_filepath: Optional[
25-
Union[Union[str, PossibleFilePath], List[Union[str, PossibleFilePath]]]
26-
]
33+
cred_filepath: Optional[Union[Union[str, Path], List[Union[str, Path]]]] = None
2734
"""Filepath to yaml file with credentials for osw and connected services"""
28-
cert_filepath: Optional[
29-
Union[Union[str, PossibleFilePath], List[Union[str, PossibleFilePath]]]
30-
]
35+
cert_filepath: Optional[Union[Union[str, Path], List[Union[str, Path]]]] = None
3136
"""Filepath to the certificates for osw and connected services"""
3237

33-
_credentials: List[BaseCredential] = PrivateAttr([])
38+
_credentials: List[CredentialManager.BaseCredential] = PrivateAttr([])
3439
"""in memory credential store"""
3540

3641
class BaseCredential(OswBaseModel):
@@ -90,15 +95,47 @@ def __init__(self, **data):
9095
if not isinstance(self.cred_filepath, list):
9196
self.cred_filepath = [self.cred_filepath]
9297
self.cred_filepath = [Path(fp) for fp in self.cred_filepath if fp != ""]
93-
# Make sure to at least warn the user if they pass cred_filepath instead of
94-
# cred_filepath
95-
attribute_names = self.__dict__.keys()
96-
unexpected_kwargs = [key for key in data.keys() if key not in attribute_names]
97-
if unexpected_kwargs:
98-
warn(f"Unexpected keyword argument(s): {', '.join(unexpected_kwargs)}")
98+
99+
@staticmethod
100+
def _oold_to_osw(oold_cred) -> CredentialManager.BaseCredential:
101+
"""Convert an oold BaseCredential to an osw credential (plain str passwords)."""
102+
from oold.backend.auth import OAuth1Credential as _OoldOAuth1
103+
104+
if isinstance(oold_cred, _OoldOAuth1):
105+
return CredentialManager.OAuth1Credential(
106+
iri=oold_cred.iri,
107+
consumer_token=oold_cred.consumer_token,
108+
consumer_secret=_secret_to_str(oold_cred.consumer_secret),
109+
access_token=oold_cred.access_token,
110+
access_secret=_secret_to_str(oold_cred.access_secret),
111+
)
112+
if isinstance(oold_cred, _OoldUserPwdCredential):
113+
return CredentialManager.UserPwdCredential(
114+
iri=oold_cred.iri,
115+
username=oold_cred.username,
116+
password=_secret_to_str(oold_cred.password),
117+
)
118+
return CredentialManager.BaseCredential(iri=oold_cred.iri)
119+
120+
def _load_file_credentials(self):
121+
"""Load credentials from YAML files using oold, return as dict."""
122+
all_creds = {}
123+
if self.cred_filepath:
124+
for fp in self.cred_filepath:
125+
fp = Path(fp)
126+
if not fp.exists():
127+
continue
128+
try:
129+
loaded = _load_credentials(fp, into_store=False)
130+
all_creds.update(loaded)
131+
except Exception as exc:
132+
print(exc)
133+
return all_creds
99134

100135
def get_credential(self, config: CredentialConfig) -> BaseCredential:
101-
"""Reads credentials from a yaml file or the in memory store
136+
"""Reads credentials from a yaml file or the in memory store.
137+
138+
Uses oold.backend.auth.find_credential for IRI matching.
102139
103140
Parameters
104141
----------
@@ -111,78 +148,36 @@ def get_credential(self, config: CredentialConfig) -> BaseCredential:
111148
Credential, contain attributes 'username' and 'password' and
112149
the matching iri.
113150
"""
151+
oold_creds = self._load_file_credentials()
114152

115-
_file_credentials: List[CredentialManager.BaseCredential] = []
116-
if self.cred_filepath:
117-
filepaths = self.cred_filepath
118-
if type(filepaths) is not list:
119-
filepaths = [filepaths]
153+
for osw_cred in self._credentials:
154+
oold_creds[osw_cred.iri] = osw_cred
120155

121-
for filepath in filepaths:
122-
if not filepath.exists():
123-
continue
124-
with open(filepath, "r", encoding="utf-8") as stream:
125-
try:
126-
accounts = yaml.safe_load(stream)
127-
if accounts is None: # Catch empty file
128-
continue
129-
for iri in accounts.keys():
130-
if (
131-
"username" in accounts[iri]
132-
and "password" in accounts[iri]
133-
):
134-
cred = CredentialManager.UserPwdCredential(
135-
username=accounts[iri]["username"],
136-
password=accounts[iri]["password"],
137-
iri=iri,
138-
)
139-
_file_credentials.append(cred)
140-
if (
141-
"consumer_token" in accounts[iri]
142-
and "consumer_secret" in accounts[iri]
143-
and "access_token" in accounts[iri]
144-
and "access_secret" in accounts[iri]
145-
):
146-
cred = CredentialManager.OAuth1Credential(
147-
consumer_token=accounts[iri]["consumer_token"],
148-
consumer_secret=accounts[iri]["consumer_secret"],
149-
access_token=accounts[iri]["access_token"],
150-
access_secret=accounts[iri]["access_secret"],
151-
iri=iri,
152-
)
153-
_file_credentials.append(cred)
154-
except yaml.YAMLError as exc:
155-
print(exc)
156-
157-
match_iri = ""
158-
cred = None
159-
creds = _file_credentials + self._credentials
160-
for _cred in creds:
161-
iri = _cred.iri
162-
if config.iri in iri:
163-
if match_iri == "" or len(match_iri) > len(
164-
iri
165-
): # use the less specific match
166-
match_iri = iri
167-
cred = _cred
168-
169-
if cred is None:
170-
if config.fallback is CredentialManager.CredentialFallback.ask:
171-
if self.cred_filepath:
172-
filepath_str = "', '".join([str(fp) for fp in self.cred_filepath])
173-
print(
174-
f"No credentials for {config.iri} found in path '{filepath_str}'. "
175-
f"Please use the prompt to login"
176-
)
177-
username = input("Enter username: ")
178-
password = getpass.getpass("Enter password: ")
179-
cred = CredentialManager.UserPwdCredential(
180-
username=username, password=password, iri=config.iri
156+
match = _find_credential(config.iri, oold_creds)
157+
158+
if match is not None:
159+
if isinstance(match, CredentialManager.BaseCredential):
160+
return match
161+
return self._oold_to_osw(match)
162+
163+
if config.fallback is CredentialManager.CredentialFallback.ask:
164+
if self.cred_filepath:
165+
filepath_str = "', '".join([str(fp) for fp in self.cred_filepath])
166+
print(
167+
f"No credentials for {config.iri} found in path '{filepath_str}'. "
168+
f"Please use the prompt to login"
181169
)
182-
self.add_credential(cred)
183-
if self.cred_filepath:
184-
self.save_credentials_to_file()
185-
return cred
170+
username = input("Enter username: ")
171+
password = getpass.getpass("Enter password: ")
172+
cred = CredentialManager.UserPwdCredential(
173+
username=username, password=password, iri=config.iri
174+
)
175+
self.add_credential(cred)
176+
if self.cred_filepath:
177+
self.save_credentials_to_file()
178+
return cred
179+
180+
return None
186181

187182
def add_credential(self, cred: BaseCredential):
188183
"""adds a credential to the in memory store
@@ -232,7 +227,7 @@ def iri_in_file(self, iri: str) -> bool:
232227
with open(fp, "r", encoding="utf-8") as stream:
233228
try:
234229
accounts = yaml.safe_load(stream)
235-
if accounts is None: # Catch empty file
230+
if accounts is None:
236231
continue
237232
for iri_ in accounts.keys():
238233
if iri_ == iri:
@@ -243,7 +238,7 @@ def iri_in_file(self, iri: str) -> bool:
243238

244239
def save_credentials_to_file(
245240
self,
246-
filepath: Union[str, PossibleFilePath] = None,
241+
filepath: Union[str, Path] = None,
247242
set_cred_filepath: bool = False,
248243
):
249244
"""Saves the in memory credentials to a file
@@ -259,13 +254,11 @@ def save_credentials_to_file(
259254
cred_filepath of the CredentialManager is not changed.
260255
"""
261256
cred_filepaths = [filepath]
262-
"""The filepath to save the credentials to."""
263257
if filepath is None:
264258
cred_filepaths = self.cred_filepath
265259
if self.cred_filepath is None:
266260
cred_filepaths = [default_paths.cred_filepath]
267261
if set_cred_filepath:
268-
# Creates error if file does not exist -> Using custom FilePath
269262
self.cred_filepath = cred_filepaths
270263
for fp in cred_filepaths:
271264
file = Path(fp)
@@ -275,7 +268,7 @@ def save_credentials_to_file(
275268
file_already_exists = file.exists()
276269
if file_already_exists:
277270
data = yaml.safe_load(file.read_text(encoding="utf-8"))
278-
if data is None: # Catch empty file
271+
if data is None:
279272
data = {}
280273
for cred in self._credentials:
281274
data[cred.iri] = cred.dict(exclude={"iri"})
@@ -316,7 +309,6 @@ def save_credentials_to_file(
316309
f"'{gitignore_fp}'."
317310
)
318311
containing_gitignore = gitignore_fp.parent.absolute()
319-
320312
if containing_gitignore in default_paths.osw_files_dir.parents:
321313
# If the default_path.osw_files_dir is a subdirectory of the directory
322314
# containing the .gitignore file, add the relative path to the

0 commit comments

Comments
 (0)