Skip to content

Commit f199283

Browse files
committed
fix: one session per host and credential, named by host alone
- a credential registered as host/path no longer becomes the WtSite iri, which built an API endpoint that does not exist - the cache is keyed by (host, credential), so one credential matching several subdomains does not serve them all the same session - pin oold to the branch carrying registered_credentials() - tests inspect the config the session is built from
1 parent f3f1080 commit f199283

3 files changed

Lines changed: 76 additions & 30 deletions

File tree

‎pyproject.toml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ classifiers = [
2121
"Topic :: Software Development :: Libraries :: Python Modules",
2222
]
2323
dependencies = [
24-
# registered_credentials(), used to find the wiki of a file without a URL
25-
"oold>=1.1.0",
24+
# TEMPORARY: registered_credentials(), used to find the wiki of a file
25+
# without a URL, is on the oold branch below and in no release (main is
26+
# 2.2.0). Replace with oold>=2.3.0 once released.
27+
"oold @ git+https://github.com/OO-LD/oold-python@feat/controller-type-lookup",
2628
"opensemantic",
2729
# TEMPORARY: the file controllers this module wraps are on the core branch
2830
# below. Replace with opensemantic.core>=0.59.0 once released.

‎src/osw/controller/file/wiki.py‎

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,15 @@ def store_params_for_upload(
8383
return {**se_params, "overwrite": "replace remote"}
8484

8585

86-
_sessions: Dict[str, Tuple[Any, OSW]] = {}
87-
"""Sessions by credential IRI, with the credential they were built from.
88-
89-
Keyed by the credential rather than by the host, because two credentials
90-
scoped to different paths of one wiki are two different logins. The credential
91-
is kept so a later ``set_credential`` replaces the session instead of being
92-
ignored for the lifetime of the process.
86+
_sessions: Dict[Tuple[str, str], Tuple[Any, OSW]] = {}
87+
"""Sessions by (host, credential IRI), with the credential they were built from.
88+
89+
The credential is part of the key because two credentials scoped to different
90+
paths of one wiki are two different logins; the host is, because one credential
91+
can match several hosts - oold's lookup treats a stored IRI contained in the
92+
searched one as a match, so a credential for ``example.org`` answers for every
93+
subdomain. The credential itself is kept so a later ``set_credential`` replaces
94+
the session instead of being ignored for the lifetime of the process.
9395
"""
9496

9597

@@ -98,34 +100,44 @@ def clear_sessions() -> None:
98100
_sessions.clear()
99101

100102

103+
def host_of(url: str) -> str:
104+
"""The wiki host a URL or a bare credential IRI names.
105+
106+
A credential is registered under whatever its owner chose - a bare host, a
107+
host with a path, a full URL. ``WtSite`` wants the host alone: given a path
108+
it builds an API endpoint that does not exist.
109+
"""
110+
parsed = urlparse(url if "//" in url else f"//{url}")
111+
return parsed.netloc or url.split("/")[0]
112+
113+
101114
def credential_for(url: str) -> Any:
102115
"""The credential registered for ``url``, or a readable error."""
103-
host = urlparse(url).netloc or url
104116
try:
105117
return _get_registered_credential(url)
106118
except ValueError as error:
107119
raise ValueError(
108-
f"No credential registered for {host}. Pass osw= explicitly or "
109-
"register one with oold.backend.auth.set_credential."
120+
f"No credential registered for {host_of(url)}. Pass osw= explicitly "
121+
"or register one with oold.backend.auth.set_credential."
110122
) from error
111123

112124

113-
def session_for(url: str, credential: Any = None) -> OSW:
125+
def session_for(url: str) -> OSW:
114126
"""An OSW session for the host of ``url``, from the registered credential.
115127
116128
The file entity may be stored somewhere other than its bytes, so the
117129
lookup key is the URL the content lives at, not the entity's own IRI.
118130
"""
119-
host = urlparse(url).netloc or url
120-
if credential is None:
121-
credential = credential_for(url)
122-
cached = _sessions.get(credential.iri)
131+
host = host_of(url)
132+
credential = credential_for(url)
133+
key = (host, credential.iri)
134+
cached = _sessions.get(key)
123135
if cached is not None and cached[0] == credential:
124136
return cached[1]
125137
cred_mngr = CredentialManager()
126138
cred_mngr.add_credential(credential)
127139
session = OSW(site=WtSite(WtSite.WtSiteConfig(iri=host, cred_mngr=cred_mngr)))
128-
_sessions[credential.iri] = (credential, session)
140+
_sessions[key] = (credential, session)
129141
return session
130142

131143

‎tests/test_wiki_file_session.py‎

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,25 @@ def test_a_session_that_was_passed_in_is_kept():
8484
# -- the session cache --------------------------------------------------------
8585

8686

87-
def test_sessions_are_cached_per_credential(monkeypatch):
88-
"""Two path-scoped credentials of one host are two logins."""
89-
built = []
87+
@pytest.fixture
88+
def built_sites(monkeypatch):
89+
"""Record the WtSite configs built, instead of connecting to a wiki."""
90+
configs = []
91+
92+
def _config(**kwargs):
93+
configs.append(kwargs)
94+
return kwargs
95+
9096
monkeypatch.setattr(
91-
wiki_module, "OSW", lambda site: built.append(site) or f"session{len(built)}"
97+
wiki_module, "OSW", lambda site: f"session{len(configs)}:{site['iri']}"
9298
)
9399
monkeypatch.setattr(wiki_module, "WtSite", lambda config: config)
94-
monkeypatch.setattr(wiki_module.WtSite, "WtSiteConfig", dict, raising=False)
100+
monkeypatch.setattr(wiki_module.WtSite, "WtSiteConfig", _config, raising=False)
101+
return configs
102+
103+
104+
def test_sessions_are_cached_per_credential(built_sites):
105+
"""Two path-scoped credentials of one host are two logins."""
95106
set_credential(_credential("wiki.example.org/a", username="a"))
96107
set_credential(_credential("wiki.example.org/b", username="b"))
97108

@@ -101,23 +112,44 @@ def test_sessions_are_cached_per_credential(monkeypatch):
101112

102113
assert first != second
103114
assert again == first
115+
# and every one of them talks to the host, not to host/path
116+
assert {config["iri"] for config in built_sites} == {"wiki.example.org"}
117+
118+
119+
def test_one_credential_does_not_collapse_two_hosts(built_sites):
120+
"""oold matches a stored IRI inside the searched one, so one credential
121+
answers for every subdomain - that must not share a session."""
122+
set_credential(_credential("example.org"))
123+
124+
first = wiki_module.session_for("https://wiki-a.example.org/wiki/File:X")
125+
second = wiki_module.session_for("https://wiki-b.example.org/wiki/File:Y")
104126

127+
assert first != second
128+
assert [config["iri"] for config in built_sites] == [
129+
"wiki-a.example.org",
130+
"wiki-b.example.org",
131+
]
132+
133+
134+
def test_a_bare_credential_iri_still_names_the_host(built_sites):
135+
"""The no-URL path hands the credential key straight to session_for."""
136+
set_credential(_credential("wiki.example.org/w"))
137+
138+
wiki_module.session_for(_controller()._session_url())
139+
140+
assert built_sites[0]["iri"] == "wiki.example.org"
105141

106-
def test_a_replaced_credential_replaces_the_session(monkeypatch):
142+
143+
def test_a_replaced_credential_replaces_the_session(built_sites):
107144
"""A later set_credential must not be ignored for the process lifetime."""
108-
built = []
109-
monkeypatch.setattr(
110-
wiki_module, "OSW", lambda site: built.append(site) or f"session{len(built)}"
111-
)
112-
monkeypatch.setattr(wiki_module, "WtSite", lambda config: config)
113-
monkeypatch.setattr(wiki_module.WtSite, "WtSiteConfig", dict, raising=False)
114145
set_credential(_credential("wiki.example.org", password="old"))
115146
first = wiki_module.session_for("https://wiki.example.org/File:X")
116147

117148
set_credential(_credential("wiki.example.org", password="new"))
118149
second = wiki_module.session_for("https://wiki.example.org/File:X")
119150

120151
assert second != first
152+
assert len(built_sites) == 2
121153

122154

123155
def test_a_missing_credential_is_reported_with_the_host():

0 commit comments

Comments
 (0)