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-release.git
The following commit(s) were added to refs/heads/main by this push:
new 2ebcac9 Add a form at the bottom of the sidebar to toggle admin status
2ebcac9 is described below
commit 2ebcac90095272cd24fa850ff8df8b9e4987d3bc
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Apr 10 19:57:45 2025 +0100
Add a form at the bottom of the sidebar to toggle admin status
---
atr/blueprints/admin/admin.py | 26 ++++++++++++++++++++++++++
atr/routes/__init__.py | 4 ----
atr/routes/draft.py | 1 -
atr/server.py | 1 +
atr/templates/draft-tools.html | 2 +-
atr/templates/includes/sidebar.html | 25 +++++++++++++++++++++----
atr/util.py | 22 ++++++++++++++++++++++
7 files changed, 71 insertions(+), 10 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 7545dfd..759e642 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -449,6 +449,32 @@ async def admin_test_kv() -> str:
"""
[email protected]("/toggle-admin-view", methods=["POST"])
+async def admin_toggle_view() -> response.Response:
+ web_session = await session.read()
+ if web_session is None:
+ # For the type checker
+ # We should pass this as an argument, then it's guaranteed
+ raise base.ASFQuartException("Not authenticated", 401)
+ user_uid = web_session.uid
+ if user_uid is None:
+ raise base.ASFQuartException("Invalid session, uid is None", 500)
+
+ app = asfquart.APP
+ if not hasattr(app, "app_id") or not isinstance(app.app_id, str):
+ raise TypeError("Internal error: APP has no valid app_id")
+
+ cookie_id = app.app_id
+ session_dict = quart.session.get(cookie_id, {})
+ downgrade = not session_dict.get("downgrade_admin_to_user", False)
+ session_dict["downgrade_admin_to_user"] = downgrade
+
+ message = "Viewing as regular user" if downgrade else "Viewing as admin"
+ await quart.flash(message, "success")
+ referrer = quart.request.referrer
+ return quart.redirect(referrer or quart.url_for("admin.admin_data"))
+
+
@admin.BLUEPRINT.route("/releases")
async def admin_releases() -> str:
"""Display a list of all releases across all stages and phases."""
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index 87225d2..872a5ca 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -417,10 +417,6 @@ class CommitterSession:
# async def user_committees(self) -> list[models.Committee]:
# return ...
- @property
- def user_is_admin(self) -> bool:
- return user.is_admin(self.uid)
-
@property
async def user_projects(self) -> list[models.Project]:
if self._projects is None:
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index d7cfa47..ffebf23 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -850,7 +850,6 @@ async def tools(session: routes.CommitterSession,
project_name: str, version_nam
file_data=file_data,
release=release,
format_file_size=routes.format_file_size,
- user_is_admin=session.user_is_admin,
)
diff --git a/atr/server.py b/atr/server.py
index 141408e..a87e45b 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -144,6 +144,7 @@ def app_setup_context(app: base.QuartApp) -> None:
"commit": metadata.commit,
"current_user": await asfquart.session.read(),
"is_admin_fn": user.is_admin,
+ "is_viewing_as_admin_fn": util.is_user_viewing_as_admin,
"is_committee_member_fn": user.is_committee_member,
"routes": modules,
"version": metadata.version,
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index ccb85de..31ebb80 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -46,7 +46,7 @@
</form>
</div>
- {% if file_path.endswith(".tar.gz") and user_is_admin %}
+ {% if file_path.endswith(".tar.gz") and
is_viewing_as_admin_fn(current_user.uid) %}
<h3>Generate SBOM</h3>
<p>Generate a CycloneDX Software Bill of Materials (SBOM) file for this
artifact.</p>
<form method="post"
diff --git a/atr/templates/includes/sidebar.html
b/atr/templates/includes/sidebar.html
index 34ef7ca..b6e174c 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -15,10 +15,11 @@
<div class="user-info">
<span>{{ current_user.fullname }}</span>
(<code>{{ current_user.uid }}</code>)
+ <br />
+ <a href="#"
+ onclick="location.href='/auth?logout=/';"
+ class="logout-link"><i class="fa-solid
fa-right-from-bracket"></i></a>
</div>
- <a href="#"
- onclick="location.href='/auth?logout=/';"
- class="logout-link"><i class="fa-solid fa-right-from-bracket"></i></a>
{% else %}
<a href="#"
@@ -102,7 +103,7 @@
</li>
</ul>
- {% if is_admin_fn(current_user.uid) %}
+ {% if is_viewing_as_admin_fn(current_user.uid) %}
<h3>Administration</h3>
<ul>
<li>
@@ -138,5 +139,21 @@
</ul>
{% endif %}
{% endif %}
+
+ {% if current_user and is_admin_fn(current_user.uid) %}
+ <h3>Admin actions</h3>
+ <form action="{{ url_for('admin.admin_toggle_view') }}"
+ method="post"
+ class="ms-2 mb-4">
+ <button type="submit" class="btn btn-sm btn-outline-secondary">
+ {% if not is_viewing_as_admin_fn(current_user.uid) %}
+ <i class="fa-solid fa-user-shield"></i> View as admin
+ {% else %}
+ <i class="fa-solid fa-user-ninja"></i> View as user
+ {% endif %}
+ </button>
+ </form>
+ {% endif %}
+
</nav>
</aside>
diff --git a/atr/util.py b/atr/util.py
index 1c87eca..117d6fe 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -32,6 +32,7 @@ from collections.abc import AsyncGenerator, Callable,
Mapping, Sequence
from typing import Annotated, Any, TypeVar
import aiofiles.os
+import asfquart
import asfquart.base as base
import asfquart.session as session
import pydantic
@@ -44,6 +45,7 @@ import quart_wtf.typing
# Therefore, this module must not import atr.db
import atr.config as config
import atr.db.models as models
+import atr.user as user
F = TypeVar("F", bound="QuartFormTyped")
T = TypeVar("T")
@@ -241,6 +243,26 @@ def get_release_dir() -> pathlib.Path:
return pathlib.Path(config.get().PHASE_STORAGE_DIR) / "release"
+def is_user_viewing_as_admin(uid: str | None) -> bool:
+ """Check whether a user is currently viewing the site with active admin
privileges."""
+ if not user.is_admin(uid):
+ return False
+
+ try:
+ app = asfquart.APP
+ if not hasattr(app, "app_id") or not isinstance(app.app_id, str):
+ _LOGGER.error("Cannot get valid app_id to read session for admin
view check")
+ return True
+
+ cookie_id = app.app_id
+ session_dict = quart.session.get(cookie_id, {})
+ is_downgraded = session_dict.get("downgrade_admin_to_user", False)
+ return not is_downgraded
+ except Exception:
+ _LOGGER.exception(f"Error checking admin downgrade session status for
{uid}")
+ return True
+
+
async def paths_recursive(base_path: pathlib.Path, sort: bool = True) ->
list[pathlib.Path]:
"""List all paths recursively in alphabetical order from a given base
path."""
paths: list[pathlib.Path] = []
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]