This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/main by this push:
new 7089b4c Cache admins from LDAP using a server task
7089b4c is described below
commit 7089b4cebcd0847c5e953cf7a4f75c5537f5c170
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Jan 23 16:03:29 2026 +0000
Cache admins from LDAP using a server task
---
atr/server.py | 10 +++++++
atr/tasks/__init__.py | 2 +-
tests/unit/test_cache.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++
tests/unit/test_ldap.py | 46 ++++++++++++++++++++++++++++++++
4 files changed, 126 insertions(+), 1 deletion(-)
diff --git a/atr/server.py b/atr/server.py
index c979fbe..0408f29 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -46,6 +46,7 @@ import werkzeug.routing as routing
import atr
import atr.blueprints as blueprints
+import atr.cache as cache
import atr.config as config
import atr.db as db
import atr.db.interaction as interaction
@@ -244,6 +245,10 @@ def _app_setup_lifecycle(app: base.QuartApp, app_config:
type[config.AppConfig])
await asyncio.to_thread(_set_file_permissions_to_read_only)
+ await cache.admins_startup_load()
+ admins_task = asyncio.create_task(cache.admins_refresh_loop())
+ app.extensions["admins_task"] = admins_task
+
worker_manager = manager.get_worker_manager()
await worker_manager.start()
@@ -282,6 +287,11 @@ def _app_setup_lifecycle(app: base.QuartApp, app_config:
type[config.AppConfig])
with contextlib.suppress(asyncio.CancelledError):
await task
+ if task := app.extensions.get("admins_task"):
+ task.cancel()
+ with contextlib.suppress(asyncio.CancelledError):
+ await task
+
await db.shutdown_database()
if audit_listener := app.extensions.get("audit_listener"):
diff --git a/atr/tasks/__init__.py b/atr/tasks/__init__.py
index 325a7e6..38d57d6 100644
--- a/atr/tasks/__init__.py
+++ b/atr/tasks/__init__.py
@@ -60,7 +60,7 @@ async def asc_checks(asf_uid: str, release: sql.Release,
revision: str, signatur
return tasks
-async def clear_scheduled(caller_data: db.Session | None = None):
+async def clear_scheduled(caller_data: db.Session | None = None) -> None:
"""Clear all future scheduled tasks of the given types."""
async with db.ensure_session(caller_data) as data:
via = sql.validate_instrumented_attribute
diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py
index 4aba1f9..a8f962f 100644
--- a/tests/unit/test_cache.py
+++ b/tests/unit/test_cache.py
@@ -29,11 +29,23 @@ if TYPE_CHECKING:
from pytest import MonkeyPatch
+class _MockApp:
+ def __init__(self):
+ self.extensions: dict[str, object] = {}
+
+
class _MockConfig:
def __init__(self, state_dir: pathlib.Path):
self.STATE_DIR = str(state_dir)
[email protected]
+def mock_app(monkeypatch: "MonkeyPatch") -> _MockApp:
+ app = _MockApp()
+ monkeypatch.setattr("asfquart.APP", app)
+ return app
+
+
@pytest.fixture
def state_dir(tmp_path: pathlib.Path, monkeypatch: "MonkeyPatch") ->
pathlib.Path:
monkeypatch.setattr("atr.config.get", lambda: _MockConfig(tmp_path))
@@ -130,3 +142,60 @@ async def
test_admins_save_to_file_creates_parent_dirs(state_dir: pathlib.Path):
await cache.admins_save_to_file(frozenset({"alice"}))
assert cache_dir.exists()
assert cache_dir.is_dir()
+
+
[email protected]
+async def test_admins_startup_load_calls_ldap_when_cache_missing(
+ state_dir: pathlib.Path, mock_app: _MockApp, monkeypatch: "MonkeyPatch"
+):
+ ldap_called = False
+
+ async def mock_fetch_admin_users() -> frozenset[str]:
+ nonlocal ldap_called
+ ldap_called = True
+ return frozenset({"ldap_alice", "ldap_bob"})
+
+ monkeypatch.setattr("atr.ldap.fetch_admin_users", mock_fetch_admin_users)
+
+ await cache.admins_startup_load()
+
+ assert ldap_called is True
+ assert mock_app.extensions["admins"] == frozenset({"ldap_alice",
"ldap_bob"})
+ cache_path = state_dir / "cache" / "admins.json"
+ assert cache_path.exists()
+
+
[email protected]
+async def test_admins_startup_load_handles_ldap_failure(
+ state_dir: pathlib.Path, mock_app: _MockApp, monkeypatch: "MonkeyPatch"
+):
+ async def mock_fetch_admin_users() -> frozenset[str]:
+ raise ConnectionError("LDAP server unavailable")
+
+ monkeypatch.setattr("atr.ldap.fetch_admin_users", mock_fetch_admin_users)
+
+ await cache.admins_startup_load()
+
+ assert "admins" not in mock_app.extensions
+ cache_path = state_dir / "cache" / "admins.json"
+ assert not cache_path.exists()
+
+
[email protected]
+async def test_admins_startup_load_uses_cache_when_present(
+ state_dir: pathlib.Path, mock_app: _MockApp, monkeypatch: "MonkeyPatch"
+):
+ ldap_called = False
+
+ async def mock_fetch_admin_users() -> frozenset[str]:
+ nonlocal ldap_called
+ ldap_called = True
+ return frozenset({"from_ldap"})
+
+ monkeypatch.setattr("atr.ldap.fetch_admin_users", mock_fetch_admin_users)
+
+ await cache.admins_save_to_file(frozenset({"cached_alice", "cached_bob"}))
+ await cache.admins_startup_load()
+
+ assert ldap_called is False
+ assert mock_app.extensions["admins"] == frozenset({"cached_alice",
"cached_bob"})
diff --git a/tests/unit/test_ldap.py b/tests/unit/test_ldap.py
index 5a51481..43fdf8a 100644
--- a/tests/unit/test_ldap.py
+++ b/tests/unit/test_ldap.py
@@ -15,16 +15,62 @@
# specific language governing permissions and limitations
# under the License.
+import pathlib
+from typing import TYPE_CHECKING
+
import pytest
+import atr.cache as cache
import atr.ldap as ldap
+if TYPE_CHECKING:
+ from pytest import MonkeyPatch
+
+
+class _MockApp:
+ def __init__(self):
+ self.extensions: dict[str, object] = {}
+
+
+class _MockConfig:
+ def __init__(self, state_dir: pathlib.Path, ldap_bind_dn: str | None,
ldap_bind_password: str | None):
+ self.STATE_DIR = str(state_dir)
+ self.LDAP_BIND_DN = ldap_bind_dn
+ self.LDAP_BIND_PASSWORD = ldap_bind_password
+
@pytest.fixture
def ldap_configured() -> bool:
return ldap.get_bind_credentials() is not None
[email protected]
+async def test_admins_startup_load_fetches_real_admins(
+ ldap_configured: bool, tmp_path: pathlib.Path, monkeypatch: "MonkeyPatch"
+):
+ _skip_if_unavailable(ldap_configured)
+
+ import atr.config as config
+
+ real_config = config.get()
+ mock_config = _MockConfig(tmp_path, real_config.LDAP_BIND_DN,
real_config.LDAP_BIND_PASSWORD)
+ monkeypatch.setattr("atr.config.get", lambda: mock_config)
+
+ mock_app = _MockApp()
+ monkeypatch.setattr("asfquart.APP", mock_app)
+
+ await cache.admins_startup_load()
+
+ admins = mock_app.extensions.get("admins")
+ assert admins is not None
+ assert isinstance(admins, frozenset)
+ assert len(admins) > 1
+ assert "wave" in admins
+
+ cache_path = tmp_path / "cache" / "admins.json"
+ assert cache_path.exists()
+
+
@pytest.mark.asyncio
async def
test_fetch_admin_users_contains_only_nonempty_strings(ldap_configured: bool):
_skip_if_unavailable(ldap_configured)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]