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 bad1c58 Allow users to cache their session data in debug mode
bad1c58 is described below
commit bad1c58996f87e470bdc72438064abb2850859f3
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Oct 24 16:01:46 2025 +0100
Allow users to cache their session data in debug mode
---
atr/principal.py | 15 +++++
atr/routes/__init__.py | 2 +
atr/routes/user.py | 152 +++++++++++++++++++++++++++++++++++++++++++++++++
atr/ssh.py | 1 +
atr/util.py | 36 ++++++++++++
5 files changed, 206 insertions(+)
diff --git a/atr/principal.py b/atr/principal.py
index dd70885..72cc894 100644
--- a/atr/principal.py
+++ b/atr/principal.py
@@ -28,6 +28,7 @@ import atr.config as config
import atr.ldap as ldap
import atr.log as log
import atr.route as route
+import atr.util as util
LDAP_CHAIRS_BASE = "cn=pmc-chairs,ou=groups,ou=services,dc=apache,dc=org"
LDAP_DN = "uid=%s,ou=people,dc=apache,dc=org"
@@ -305,6 +306,20 @@ class AuthoriserLDAP:
self.__cache.last_refreshed[asf_uid] = int(time.time())
return
+ if config.get_mode() == config.Mode.Debug:
+ session_cache = await util.session_cache_read()
+ if asf_uid in session_cache:
+ cached_session = session_cache[asf_uid]
+ committees = frozenset(cached_session.get("pmcs", []))
+ projects = frozenset(cached_session.get("projects", []))
+ committees, projects = _augment_test_membership(committees,
projects)
+
+ self.__cache.member_of[asf_uid] = committees
+ self.__cache.participant_of[asf_uid] = projects
+ self.__cache.last_refreshed[asf_uid] = int(time.time())
+ log.info(f"Loaded session data for {asf_uid} from session
cache file")
+ return
+
try:
c = Committer(asf_uid)
await asyncio.to_thread(c.verify)
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index b6e8ab3..0d64cae 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -40,6 +40,7 @@ import atr.routes.sbom as sbom
import atr.routes.start as start
import atr.routes.tokens as tokens
import atr.routes.upload as upload
+import atr.routes.user as user
import atr.routes.vote as vote
import atr.routes.voting as voting
@@ -69,6 +70,7 @@ __all__ = [
"start",
"tokens",
"upload",
+ "user",
"vote",
"voting",
]
diff --git a/atr/routes/user.py b/atr/routes/user.py
new file mode 100644
index 0000000..7ae4a47
--- /dev/null
+++ b/atr/routes/user.py
@@ -0,0 +1,152 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import htpy
+import quart
+
+import atr.forms as forms
+import atr.htm as htm
+import atr.route as route
+import atr.template as template
+import atr.util as util
+
+if TYPE_CHECKING:
+ import werkzeug.wrappers.response as response
+
+
+class CacheForm(forms.Typed):
+ cache_submit = forms.submit("Cache me!")
+
+
+class DeleteCacheForm(forms.Typed):
+ delete_submit = forms.submit("Delete my cache")
+
+
[email protected]("/user/cache", methods=["GET"])
+async def cache_get(session: route.CommitterSession) -> str:
+ cache_form = await CacheForm.create_form()
+ delete_cache_form = await DeleteCacheForm.create_form()
+
+ cache_data = await util.session_cache_read()
+ user_cached = session.uid in cache_data
+
+ block = htm.Block()
+
+ block.h1["Session cache management"]
+
+ block.p[
+ """This page allows you to cache your ASFQuart session information for
use in
+ contexts where web authentication is not available, such as SSH and
rsync, the
+ API, and background tasks. This is intended for developers only."""
+ ]
+
+ if user_cached:
+ cached_entry = cache_data[session.uid]
+ block.h2["Your cached session"]
+ block.p["Your session is currently cached."]
+
+ tbody = htm.Block(htpy.tbody)
+ tbody.append(htpy.tr[htpy.th["User ID"], htpy.td[session.uid]])
+ if "fullname" in cached_entry:
+ tbody.append(htpy.tr[htpy.th["Full name"],
htpy.td[cached_entry["fullname"]]])
+ if "email" in cached_entry:
+ tbody.append(htpy.tr[htpy.th["Email"],
htpy.td[cached_entry["email"]]])
+ if "pmcs" in cached_entry:
+ committees = ", ".join(cached_entry["pmcs"]) if
cached_entry["pmcs"] else "-"
+ tbody.append(htpy.tr[htpy.th["Committees"], htpy.td[committees]])
+ if "projects" in cached_entry:
+ projects = ", ".join(cached_entry["projects"]) if
cached_entry["projects"] else "-"
+ tbody.append(htpy.tr[htpy.th["Projects"], htpy.td[projects]])
+
+ block.table(".table.table-striped.table-bordered")[tbody.collect()]
+
+ block.h3["Delete cache"]
+ block.p["Remove your cached session information:"]
+ delete_form_element = forms.render_simple(
+ delete_cache_form,
+ action=quart.request.path,
+ submit_classes="btn-danger",
+ )
+ block.append(delete_form_element)
+ else:
+ block.h2["No cached session"]
+ block.p["Your session is not currently cached."]
+
+ block.h3["Cache current session"]
+ block.p["Press the button below to cache your current session
information:"]
+ cache_form_element = forms.render_simple(
+ cache_form,
+ action=quart.request.path,
+ submit_classes="btn-primary",
+ )
+ block.append(cache_form_element)
+
+ return await template.blank("Session cache management",
content=block.collect())
+
+
[email protected]("/user/cache", methods=["POST"])
+async def session_post(session: route.CommitterSession) -> response.Response:
+ form_data = await quart.request.form
+
+ cache_form = await CacheForm.create_form(data=form_data)
+ delete_cache_form = await DeleteCacheForm.create_form(data=form_data)
+
+ if cache_form.cache_submit.data:
+ await _cache_session(session)
+ await quart.flash("Your session has been cached successfully",
"success")
+ elif delete_cache_form.delete_submit.data:
+ await _delete_session_cache(session)
+ await quart.flash("Your cached session has been deleted", "success")
+ else:
+ await quart.flash("Invalid form submission", "error")
+
+ return await session.redirect(cache_get)
+
+
+async def _cache_session(session: route.CommitterSession) -> None:
+ cache_data = await util.session_cache_read()
+
+ session_data = {
+ "uid": session.uid,
+ "dn": getattr(session, "dn", None),
+ "fullname": getattr(session, "fullname", None),
+ "email": getattr(session, "email", f"{session.uid}@apache.org"),
+ "isMember": getattr(session, "isMember", False),
+ "isChair": getattr(session, "isChair", False),
+ "isRoot": getattr(session, "isRoot", False),
+ "pmcs": getattr(session, "committees", []),
+ "projects": getattr(session, "projects", []),
+ "mfa": getattr(session, "mfa", False),
+ "roleaccount": getattr(session, "isRole", False),
+ "metadata": getattr(session, "metadata", {}),
+ }
+
+ cache_data[session.uid] = session_data
+
+ await util.session_cache_write(cache_data)
+
+
+async def _delete_session_cache(session: route.CommitterSession) -> None:
+ cache_data = await util.session_cache_read()
+
+ if session.uid in cache_data:
+ del cache_data[session.uid]
+ await util.session_cache_write(cache_data)
diff --git a/atr/ssh.py b/atr/ssh.py
index 862d8b5..6e04cb9 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -353,6 +353,7 @@ async def _step_06a_validate_read_permissions(
sql.ReleasePhase.RELEASE_CANDIDATE,
sql.ReleasePhase.RELEASE_PREVIEW,
}
+ print(release)
if release.phase not in allowed_read_phases:
raise RsyncArgsError(f"Release '{release.name}' is not in a readable
phase ({release.phase.value})")
diff --git a/atr/util.py b/atr/util.py
index 3909aba..c0b5208 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -774,6 +774,42 @@ def release_directory_version(release: sql.Release) ->
pathlib.Path:
return path
+async def session_cache_read() -> dict[str, dict]:
+ cache_path = pathlib.Path(config.get().STATE_DIR) /
"user_session_cache.json"
+ try:
+ async with aiofiles.open(cache_path) as f:
+ content = await f.read()
+ return json.loads(content)
+ except FileNotFoundError:
+ return {}
+ except json.JSONDecodeError:
+ return {}
+
+
+async def session_cache_write(cache_data: dict[str, dict]) -> None:
+ cache_path = pathlib.Path(config.get().STATE_DIR) /
"user_session_cache.json"
+
+ cache_dir = cache_path.parent
+ await asyncio.to_thread(os.makedirs, cache_dir, exist_ok=True)
+
+ # Use the same pattern as update_atomic_symlink for the temporary file name
+ temp_path = cache_dir / f".{cache_path.name}.{uuid.uuid4()}.tmp"
+
+ try:
+ async with aiofiles.open(temp_path, "w") as f:
+ await f.write(json.dumps(cache_data, indent=2))
+ await f.flush()
+ await asyncio.to_thread(os.fsync, f.fileno())
+
+ await aiofiles.os.rename(temp_path, cache_path)
+ except Exception:
+ try:
+ await aiofiles.os.remove(temp_path)
+ except FileNotFoundError:
+ pass
+ raise
+
+
def static_path(*args: str) -> str:
filename = str(pathlib.PurePosixPath(*args))
return quart.url_for("static", filename=filename)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]