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 65dfe39 Move admin routes into the improved blueprints layout
65dfe39 is described below
commit 65dfe39a99a29d0ebccfa54a06b4814cb8350db5
Author: Sean B. Palmer <[email protected]>
AuthorDate: Sun Oct 26 20:03:45 2025 +0000
Move admin routes into the improved blueprints layout
---
atr/{bps/admin/admin.py => admin/__init__.py} | 284 ++++++++++++---------
atr/{bps => }/admin/templates/all-releases.html | 0
atr/{bps => }/admin/templates/browse-as.html | 0
atr/{bps => }/admin/templates/data-browser.html | 2 +-
atr/{bps => }/admin/templates/delete-release.html | 0
atr/{bps => }/admin/templates/ldap-lookup.html | 2 +-
atr/{bps => }/admin/templates/performance.html | 0
atr/{bps => }/admin/templates/tasks.html | 0
.../admin/templates/toggle-admin-view.html | 2 +-
atr/{bps => }/admin/templates/update-keys.html | 0
atr/{bps => }/admin/templates/update-projects.html | 0
atr/{bps => }/admin/templates/validation.html | 0
atr/blueprints/__init__.py | 2 +
atr/blueprints/{get.py => admin.py} | 75 +++---
atr/blueprints/get.py | 11 +-
atr/bps/__init__.py | 2 -
atr/bps/admin/__init__.py | 43 ----
atr/principal.py | 16 +-
atr/server.py | 2 +
atr/templates/delete-committee-keys.html | 2 +-
atr/templates/includes/sidebar.html | 64 ++---
21 files changed, 253 insertions(+), 254 deletions(-)
diff --git a/atr/bps/admin/admin.py b/atr/admin/__init__.py
similarity index 81%
rename from atr/bps/admin/admin.py
rename to atr/admin/__init__.py
index 01972c1..2230452 100644
--- a/atr/bps/admin/admin.py
+++ b/atr/admin/__init__.py
@@ -23,18 +23,18 @@ import statistics
import sys
import time
from collections.abc import Callable, Mapping
-from typing import Any
+from typing import Any, Final, Literal
import aiofiles.os
import aiohttp
import asfquart
import asfquart.base as base
-import asfquart.session as session
+import asfquart.session
import quart
import sqlalchemy.orm as orm
import werkzeug.wrappers.response as response
-import atr.bps.admin as admin
+import atr.blueprints.admin as admin
import atr.config as config
import atr.datasources.apache as apache
import atr.db as db
@@ -44,8 +44,8 @@ import atr.ldap as ldap
import atr.log as log
import atr.models.sql as sql
import atr.principal as principal
-import atr.route as route
import atr.routes.mapping as mapping
+import atr.session as session
import atr.storage as storage
import atr.storage.outcome as outcome
import atr.storage.types as types
@@ -54,6 +54,8 @@ import atr.template as template
import atr.util as util
import atr.validate as validate
+ROUTES_MODULE: Final[Literal[True]] = True
+
class BrowseAsUserForm(forms.Typed):
"""Form for browsing as another user."""
@@ -90,16 +92,25 @@ class LdapLookupForm(forms.Typed):
submit = forms.submit("Lookup")
[email protected]("/all-releases")
-async def admin_all_releases() -> str:
[email protected]("/all-releases")
+async def all_releases(session: session.Committer) -> str:
"""Display a list of all releases across all phases."""
async with db.session() as data:
releases = await data.release(_project=True,
_committee=True).order_by(sql.Release.name).all()
return await template.render("all-releases.html", releases=releases,
release_as_url=mapping.release_as_url)
[email protected]("/browse-as", methods=["GET", "POST"])
-async def admin_browse_as() -> str | response.Response:
[email protected]("/browse-as")
+async def browse_as_get(session: session.Committer) -> str | response.Response:
+ return await _browse_as(session)
+
+
[email protected]("/browse-as")
+async def browse_as_post(session: session.Committer) -> str |
response.Response:
+ return await _browse_as(session)
+
+
+async def _browse_as(session: session.Committer) -> str | response.Response:
"""Allows an admin to browse as another user."""
# TODO: Enable this in debugging mode only?
from atr.routes import root
@@ -109,7 +120,7 @@ async def admin_browse_as() -> str | response.Response:
return await template.render("browse-as.html", form=form)
new_uid = str(util.unwrap(form.uid.data))
- if not (current_session := await session.read()):
+ if not (current_session := await asfquart.session.read()):
raise base.ASFQuartException("Not authenticated", 401)
bind_dn, bind_password = principal.get_ldap_bind_dn_and_password()
@@ -122,7 +133,7 @@ async def admin_browse_as() -> str | response.Response:
if not ldap_params.results_list:
await quart.flash(f"User '{new_uid}' not found in LDAP.", "error")
- return quart.redirect(quart.url_for("admin.admin_browse_as"))
+ return await session.redirect(browse_as_get)
ldap_projects_data = await apache.get_ldap_projects_data()
committee_data = await apache.get_active_committee_data()
@@ -138,17 +149,17 @@ async def admin_browse_as() -> str | response.Response:
bind_password,
)
log.info("New Quart cookie (not ASFQuart session) data: %s",
new_session_data)
- session.write(new_session_data)
+ asfquart.session.write(new_session_data)
await quart.flash(
f"You are now browsing as '{new_uid}'. To return to your own account,
please log out and log back in.",
"success",
)
- return quart.redirect(util.as_url(root.index))
+ return await session.redirect(root.index)
[email protected]("/config")
-async def admin_config() -> quart.wrappers.response.Response:
[email protected]("/configuration")
+async def configuration(session: session.Committer) ->
quart.wrappers.response.Response:
"""Display the current application configuration values."""
conf = config.get()
@@ -170,8 +181,8 @@ async def admin_config() ->
quart.wrappers.response.Response:
return quart.Response("\n".join(values), mimetype="text/plain")
[email protected]("/consistency")
-async def admin_consistency() -> quart.Response:
[email protected]("/consistency")
+async def consistency(session: session.Committer) -> quart.Response:
"""Check for consistency between the database and the filesystem."""
# Get all releases from the database
async with db.session() as data:
@@ -218,9 +229,9 @@ Paired correctly:
)
[email protected]("/data")
[email protected]("/data/<model>")
-async def admin_data(model: str = "Committee") -> str:
[email protected]("/data")
[email protected]("/data/<model>")
+async def data(session: session.Committer, model: str = "Committee") -> str:
"""Browse all records in the database."""
async with db.session() as data:
# Map of model names to their classes
@@ -267,9 +278,20 @@ async def admin_data(model: str = "Committee") -> str:
)
[email protected]("/delete-test-openpgp-keys", methods=["GET", "POST"])
-async def admin_delete_test_openpgp_keys() -> quart.Response |
response.Response:
[email protected]("/delete-test-openpgp-keys")
+async def delete_test_openpgp_keys_get(session: session.Committer) ->
quart.Response | response.Response:
+ return await _delete_test_openpgp_keys(session)
+
+
[email protected]("/delete-test-openpgp-keys")
+async def delete_test_openpgp_keys_post(session: session.Committer) ->
quart.Response | response.Response:
+ return await _delete_test_openpgp_keys(session)
+
+
+async def _delete_test_openpgp_keys(session: session.Committer) ->
quart.Response | response.Response:
"""Delete all test user OpenPGP keys and their links."""
+ import atr.routes
+
if not config.get().ALLOW_TESTS:
raise base.ASFQuartException("Test operations are disabled in this
environment", errorcode=403)
@@ -295,11 +317,20 @@ async def admin_delete_test_openpgp_keys() ->
quart.Response | response.Response
outcome = await wafc.keys.test_user_delete_all(test_uid)
outcome.result_or_raise()
- return quart.redirect("/keys")
+ return await session.redirect(atr.routes.keys.keys)
+
+
[email protected]("/delete-committee-keys")
+async def delete_committee_keys_get(session: session.Committer) -> str |
response.Response:
+ return await _delete_committee_keys(session)
+
+
[email protected]("/delete-committee-keys")
+async def delete_committee_keys_post(session: session.Committer) -> str |
response.Response:
+ return await _delete_committee_keys(session)
[email protected]("/delete-committee-keys", methods=["GET", "POST"])
-async def admin_delete_committee_keys() -> str | response.Response:
+async def _delete_committee_keys(session: session.Committer) -> str |
response.Response:
form = await DeleteCommitteeKeysForm.create_form()
async with db.session() as data:
all_committees = await
data.committee(_public_signing_keys=True).order_by(sql.Committee.name).all()
@@ -320,12 +351,12 @@ async def admin_delete_committee_keys() -> str |
response.Response:
if not committee:
await quart.flash(f"Committee '{committee_name}' not found.",
"error")
- return
quart.redirect(quart.url_for("admin.admin_delete_committee_keys"))
+ return await session.redirect(delete_committee_keys_get)
keys_to_check = list(committee.public_signing_keys)
if not keys_to_check:
await quart.flash(f"Committee '{committee_name}' has no
keys.", "info")
- return
quart.redirect(quart.url_for("admin.admin_delete_committee_keys"))
+ return await session.redirect(delete_committee_keys_get)
num_removed = len(committee.public_signing_keys)
committee.public_signing_keys.clear()
@@ -342,7 +373,7 @@ async def admin_delete_committee_keys() -> str |
response.Response:
f"Removed {num_removed} key links for '{committee_name}'.
Deleted {unused_deleted} unused keys.",
"success",
)
- return
quart.redirect(quart.url_for("admin.admin_delete_committee_keys"))
+ return await session.redirect(delete_committee_keys_get)
elif quart.request.method == "POST":
await quart.flash("Form validation failed. Select committee and type
DELETE KEYS.", "warning")
@@ -350,18 +381,20 @@ async def admin_delete_committee_keys() -> str |
response.Response:
return await template.render("delete-committee-keys.html", form=form)
[email protected]("/delete-release", methods=["GET", "POST"])
-async def admin_delete_release() -> str | response.Response:
[email protected]("/delete-release")
+async def delete_release_get(session: session.Committer) -> str |
response.Response:
+ return await _delete_release(session)
+
+
[email protected]("/delete-release")
+async def delete_release_post(session: session.Committer) -> str |
response.Response:
+ return await _delete_release(session)
+
+
+async def _delete_release(session: session.Committer) -> str |
response.Response:
"""Page to delete selected releases and their associated data and files."""
form = await DeleteReleaseForm.create_form()
- web_session = await session.read()
- if web_session is None:
- raise base.ASFQuartException("Not authenticated", 401)
- asf_uid = web_session.uid
- if asf_uid is None:
- raise base.ASFQuartException("Invalid session, uid is None", 500)
-
if quart.request.method == "POST":
if await form.validate_on_submit():
form_data = await quart.request.form
@@ -369,13 +402,12 @@ async def admin_delete_release() -> str |
response.Response:
if not releases_to_delete:
await quart.flash("No releases selected for deletion.",
"warning")
- return
quart.redirect(quart.url_for("admin.admin_delete_release"))
+ return await session.redirect(delete_release_get)
- committer_session = route.CommitterSession(web_session)
- await _delete_releases(committer_session, releases_to_delete)
+ await _delete_releases(session, releases_to_delete)
# Redirecting back to the deletion page will refresh the list of
releases too
- return quart.redirect(quart.url_for("admin.admin_delete_release"))
+ return await session.redirect(delete_release_get)
# It's unlikely that form validation failed due to spurious release
names
# Therefore we assume that the user forgot to type DELETE to confirm
@@ -388,8 +420,8 @@ async def admin_delete_release() -> str | response.Response:
return await template.render("delete-release.html", form=form,
releases=releases, stats=None)
[email protected]("/env")
-async def admin_env() -> quart.wrappers.response.Response:
[email protected]("/env")
+async def env(session: session.Committer) -> quart.wrappers.response.Response:
"""Display the environment variables."""
env_vars = []
for key, value in os.environ.items():
@@ -397,8 +429,17 @@ async def admin_env() -> quart.wrappers.response.Response:
return quart.Response("\n".join(env_vars), mimetype="text/plain")
[email protected]("/keys/check", methods=["GET", "POST"])
-async def admin_keys_check() -> quart.Response:
[email protected]("/keys/check")
+async def keys_check_get(session: session.Committer) -> quart.Response:
+ return await _keys_check(session)
+
+
[email protected]("/keys/check")
+async def keys_check_post(session: session.Committer) -> quart.Response:
+ return await _keys_check(session)
+
+
+async def _keys_check(session: session.Committer) -> quart.Response:
"""Check public signing key details."""
if quart.request.method != "POST":
empty_form = await forms.Empty.create_form()
@@ -420,8 +461,17 @@ async def admin_keys_check() -> quart.Response:
return quart.Response(f"Exception during key check: {e!s}",
mimetype="text/plain")
[email protected]("/keys/regenerate-all", methods=["GET", "POST"])
-async def admin_keys_regenerate_all() -> quart.Response:
[email protected]("/keys/regenerate-all")
+async def keys_regenerate_all_get(session: session.Committer) ->
quart.Response:
+ return await _keys_regenerate_all(session)
+
+
[email protected]("/keys/regenerate-all")
+async def keys_regenerate_all_post(session: session.Committer) ->
quart.Response:
+ return await _keys_regenerate_all(session)
+
+
+async def _keys_regenerate_all(session: session.Committer) -> quart.Response:
"""Regenerate the KEYS file for all committees."""
if quart.request.method != "POST":
empty_form = await forms.Empty.create_form()
@@ -438,13 +488,6 @@ async def admin_keys_regenerate_all() -> quart.Response:
async with db.session() as data:
committee_names = [c.name for c in await data.committee().all()]
- web_session = await session.read()
- if web_session is None:
- raise base.ASFQuartException("Not authenticated", 401)
- asf_uid = web_session.uid
- if asf_uid is None:
- raise base.ASFQuartException("Invalid session, uid is None", 500)
-
outcomes = outcome.List[str]()
async with storage.write() as write:
for committee_name in committee_names:
@@ -463,8 +506,17 @@ async def admin_keys_regenerate_all() -> quart.Response:
return quart.Response("\n".join(response_lines), mimetype="text/plain")
[email protected]("/keys/update", methods=["GET", "POST"])
-async def admin_keys_update() -> str | response.Response | tuple[Mapping[str,
Any], int]:
[email protected]("/keys/update")
+async def keys_update_get(session: session.Committer) -> str |
response.Response | tuple[Mapping[str, Any], int]:
+ return await _keys_update(session)
+
+
[email protected]("/keys/update")
+async def keys_update_post(session: session.Committer) -> str |
response.Response | tuple[Mapping[str, Any], int]:
+ return await _keys_update(session)
+
+
+async def _keys_update(session: session.Committer) -> str | response.Response
| tuple[Mapping[str, Any], int]:
"""Update keys from remote data."""
if quart.request.method != "POST":
empty_form = await forms.Empty.create_form()
@@ -478,13 +530,7 @@ async def admin_keys_update() -> str | response.Response |
tuple[Mapping[str, An
return await template.render("update-keys.html",
empty_form=empty_form, previous_output=previous_output)
try:
- web_session = await session.read()
- if web_session is None:
- raise base.ASFQuartException("Not authenticated", 401)
- asf_uid = web_session.uid
- if asf_uid is None:
- raise base.ASFQuartException("Invalid session, uid is None", 500)
- pid = await _update_keys(asf_uid)
+ pid = await _update_keys(session.asf_uid)
return {
"message": f"Successfully started key update process with PID
{pid}",
"category": "success",
@@ -497,14 +543,18 @@ async def admin_keys_update() -> str | response.Response
| tuple[Mapping[str, An
}, 200
[email protected]("/ldap/", methods=["GET"])
-async def admin_ldap() -> str:
- form = await LdapLookupForm.create_form(data=quart.request.args)
- asf_id_for_template: str | None = None
[email protected]("/ldap/")
+async def ldap_get(session: session.Committer) -> str:
+ return await _ldap(session)
+
- web_session = await session.read()
- if web_session and web_session.uid:
- asf_id_for_template = web_session.uid
[email protected]("/ldap/")
+async def ldap_post(session: session.Committer) -> str:
+ return await _ldap(session)
+
+
+async def _ldap(session: session.Committer) -> str:
+ form = await LdapLookupForm.create_form(data=quart.request.args)
uid_query = form.uid.data
email_query = form.email.data
@@ -530,14 +580,29 @@ async def admin_ldap() -> str:
"ldap-lookup.html",
form=form,
ldap_params=ldap_params,
- asf_id=asf_id_for_template,
+ asf_id=session.asf_uid,
ldap_query_performed=ldap_params is not None,
uid_query=uid_query,
)
[email protected]("/ongoing-tasks/<project_name>/<version_name>/<revision>")
-async def admin_ongoing_tasks(project_name: str, version_name: str, revision:
str) -> quart.wrappers.response.Response:
[email protected]("/ongoing-tasks/<project_name>/<version_name>/<revision>")
+async def ongoing_tasks_get(
+ session: session.Committer, project_name: str, version_name: str,
revision: str
+) -> quart.wrappers.response.Response:
+ return await _ongoing_tasks(session, project_name, version_name, revision)
+
+
[email protected]("/ongoing-tasks/<project_name>/<version_name>/<revision>")
+async def ongoing_tasks_post(
+ session: session.Committer, project_name: str, version_name: str,
revision: str
+) -> quart.wrappers.response.Response:
+ return await _ongoing_tasks(session, project_name, version_name, revision)
+
+
+async def _ongoing_tasks(
+ session: session.Committer, project_name: str, version_name: str,
revision: str
+) -> quart.wrappers.response.Response:
try:
ongoing = await interaction.tasks_ongoing(project_name, version_name,
revision)
return quart.Response(str(ongoing), mimetype="text/plain")
@@ -546,8 +611,8 @@ async def admin_ongoing_tasks(project_name: str,
version_name: str, revision: st
return quart.Response("", mimetype="text/plain")
[email protected]("/performance")
-async def admin_performance() -> str:
[email protected]("/performance")
+async def performance(session: session.Committer) -> str:
"""Display performance statistics for all routes."""
app = asfquart.APP
@@ -627,19 +692,21 @@ async def admin_performance() -> str:
return await template.render("performance.html", stats=sorted_summary)
[email protected]("/projects/update", methods=["GET", "POST"])
-async def admin_projects_update() -> str | response.Response |
tuple[Mapping[str, Any], int]:
[email protected]("/projects/update")
+async def projects_update_get(session: session.Committer) -> str |
response.Response | tuple[Mapping[str, Any], int]:
+ return await _projects_update(session)
+
+
[email protected]("/projects/update")
+async def projects_update_post(session: session.Committer) -> str |
response.Response | tuple[Mapping[str, Any], int]:
+ return await _projects_update(session)
+
+
+async def _projects_update(session: session.Committer) -> str |
response.Response | tuple[Mapping[str, Any], int]:
"""Update projects from remote data."""
if quart.request.method == "POST":
try:
- web_session = await session.read()
- if web_session is None:
- raise base.ASFQuartException("Not authenticated", 401)
- asf_uid = web_session.uid
- if asf_uid is None:
- raise base.ASFQuartException("Invalid session, uid is None",
500)
-
- task = await tasks.metadata_update(asf_uid)
+ task = await tasks.metadata_update(session.asf_uid)
return {
"message": f"Metadata update task has been queued with ID
{task.id}.",
"category": "success",
@@ -656,14 +723,14 @@ async def admin_projects_update() -> str |
response.Response | tuple[Mapping[str
return await template.render("update-projects.html", empty_form=empty_form)
[email protected]("/tasks")
-async def admin_tasks() -> str:
[email protected]("/tasks")
+async def tasks_(session: session.Committer) -> str:
return await template.render("tasks.html")
[email protected]("/task-times/<project_name>/<version_name>/<revision_number>")
-async def admin_task_times(
- project_name: str, version_name: str, revision_number: str
[email protected]("/task-times/<project_name>/<version_name>/<revision_number>")
+async def task_times(
+ session: session.Committer, project_name: str, version_name: str,
revision_number: str
) -> quart.wrappers.response.Response:
values = []
async with db.session() as data:
@@ -679,8 +746,8 @@ async def admin_task_times(
return quart.Response("\n".join(values), mimetype="text/plain")
[email protected]("/test", methods=["GET"])
-async def admin_test() -> quart.wrappers.response.Response:
[email protected]("/test")
+async def test(session: session.Committer) -> quart.wrappers.response.Response:
"""Test the storage layer."""
import atr.storage as storage
@@ -689,13 +756,7 @@ async def admin_test() -> quart.wrappers.response.Response:
async with aiohttp_client_session.get(url) as response:
keys_file_text = await response.text()
- web_session = await session.read()
- if web_session is None:
- raise base.ASFQuartException("Not authenticated", 401)
- asf_uid = web_session.uid
- if asf_uid is None:
- raise base.ASFQuartException("Invalid session, uid is None", 500)
- async with storage.write() as write:
+ async with storage.write(session) as write:
wacm = write.as_committee_member("tooling")
start = time.perf_counter_ns()
outcomes: outcome.List[types.Key] = await
wacm.keys.ensure_stored(keys_file_text)
@@ -718,26 +779,17 @@ async def admin_test() ->
quart.wrappers.response.Response:
return quart.Response(str(wacm), mimetype="text/plain")
[email protected]("/toggle-view", methods=["GET"])
-async def admin_toggle_admin_view_page() -> str:
[email protected]("/toggle-view")
+async def toggle_view_get(session: session.Committer) -> str:
"""Display the page with a button to toggle between admin and user
views."""
empty_form = await forms.Empty.create_form()
return await template.render("toggle-admin-view.html",
empty_form=empty_form)
[email protected]("/toggle-admin-view", methods=["POST"])
-async def admin_toggle_view() -> response.Response:
[email protected]("/toggle-view")
+async def toggle_view_post(session: session.Committer) -> response.Response:
await util.validate_empty_form()
- 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")
@@ -750,11 +802,11 @@ async def admin_toggle_view() -> response.Response:
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"))
+ return quart.redirect(referrer or util.as_url(data))
[email protected]("/validate")
-async def admin_validate() -> str:
[email protected]("/validate")
+async def validate_(session: session.Committer) -> str:
"""Run validators and display any divergences."""
async with db.session() as data:
@@ -789,7 +841,7 @@ async def _check_keys(fix: bool = False) -> str:
return message
-async def _delete_releases(session: route.CommitterSession,
releases_to_delete: list[str]) -> None:
+async def _delete_releases(session: session.Committer, releases_to_delete:
list[str]) -> None:
success_count = 0
fail_count = 0
error_messages = []
@@ -881,7 +933,7 @@ def _get_user_committees_from_ldap(uid: str, bind_dn: str,
bind_password: str) -
def _session_data(
ldap_data: dict[str, Any],
new_uid: str,
- current_session: session.ClientSession,
+ current_session: asfquart.session.ClientSession,
ldap_projects: apache.LDAPProjectsData,
committee_data: apache.CommitteeData,
bind_dn: str,
diff --git a/atr/bps/admin/templates/all-releases.html
b/atr/admin/templates/all-releases.html
similarity index 100%
rename from atr/bps/admin/templates/all-releases.html
rename to atr/admin/templates/all-releases.html
diff --git a/atr/bps/admin/templates/browse-as.html
b/atr/admin/templates/browse-as.html
similarity index 100%
rename from atr/bps/admin/templates/browse-as.html
rename to atr/admin/templates/browse-as.html
diff --git a/atr/bps/admin/templates/data-browser.html
b/atr/admin/templates/data-browser.html
similarity index 97%
rename from atr/bps/admin/templates/data-browser.html
rename to atr/admin/templates/data-browser.html
index be0ec1d..5337682 100644
--- a/atr/bps/admin/templates/data-browser.html
+++ b/atr/admin/templates/data-browser.html
@@ -70,7 +70,7 @@
<div class="page-model-nav">
{% for model_name in models %}
- <a href="{{ url_for('admin.admin_data', model=model_name) }}"
+ <a href="{{ as_url(admin.data, model=model_name) }}"
{% if model == model_name %}class="active"{% endif %}>{{ model_name
}}</a>
{% endfor %}
</div>
diff --git a/atr/bps/admin/templates/delete-release.html
b/atr/admin/templates/delete-release.html
similarity index 100%
rename from atr/bps/admin/templates/delete-release.html
rename to atr/admin/templates/delete-release.html
diff --git a/atr/bps/admin/templates/ldap-lookup.html
b/atr/admin/templates/ldap-lookup.html
similarity index 98%
rename from atr/bps/admin/templates/ldap-lookup.html
rename to atr/admin/templates/ldap-lookup.html
index 41e6152..ceff544 100644
--- a/atr/bps/admin/templates/ldap-lookup.html
+++ b/atr/admin/templates/ldap-lookup.html
@@ -17,7 +17,7 @@
<h5 class="mb-0">Search criteria</h5>
</div>
<div class="card-body">
- <form method="get" action="{{ url_for('admin.admin_ldap') }}">
+ <form method="get" action="{{ as_url(admin.ldap_get) }}">
{{ form.csrf_token }}
<div class="mb-3">
{{ form.uid.label(class="form-label") }}
diff --git a/atr/bps/admin/templates/performance.html
b/atr/admin/templates/performance.html
similarity index 100%
rename from atr/bps/admin/templates/performance.html
rename to atr/admin/templates/performance.html
diff --git a/atr/bps/admin/templates/tasks.html b/atr/admin/templates/tasks.html
similarity index 100%
rename from atr/bps/admin/templates/tasks.html
rename to atr/admin/templates/tasks.html
diff --git a/atr/bps/admin/templates/toggle-admin-view.html
b/atr/admin/templates/toggle-admin-view.html
similarity index 93%
rename from atr/bps/admin/templates/toggle-admin-view.html
rename to atr/admin/templates/toggle-admin-view.html
index 28e87bd..b39910b 100644
--- a/atr/bps/admin/templates/toggle-admin-view.html
+++ b/atr/admin/templates/toggle-admin-view.html
@@ -15,7 +15,7 @@
</p>
{% if current_user and is_admin_fn(current_user.uid) %}
- <form action="{{ url_for('admin.admin_toggle_view') }}" method="post"
class="mb-4">
+ <form action="{{ as_url(admin.toggle_view_post) }}" method="post"
class="mb-4">
{{ empty_form.hidden_tag() }}
<button type="submit" class="btn btn-primary">
diff --git a/atr/bps/admin/templates/update-keys.html
b/atr/admin/templates/update-keys.html
similarity index 100%
rename from atr/bps/admin/templates/update-keys.html
rename to atr/admin/templates/update-keys.html
diff --git a/atr/bps/admin/templates/update-projects.html
b/atr/admin/templates/update-projects.html
similarity index 100%
rename from atr/bps/admin/templates/update-projects.html
rename to atr/admin/templates/update-projects.html
diff --git a/atr/bps/admin/templates/validation.html
b/atr/admin/templates/validation.html
similarity index 100%
rename from atr/bps/admin/templates/validation.html
rename to atr/admin/templates/validation.html
diff --git a/atr/blueprints/__init__.py b/atr/blueprints/__init__.py
index b72501b..7d149a8 100644
--- a/atr/blueprints/__init__.py
+++ b/atr/blueprints/__init__.py
@@ -20,6 +20,7 @@ from typing import Protocol, runtime_checkable
import asfquart.base as base
+import atr.blueprints.admin as admin
import atr.blueprints.api as api
import atr.blueprints.get as get
@@ -38,5 +39,6 @@ def check_module(module: ModuleType) -> None:
def register(app: base.QuartApp) -> None:
+ check_module(admin.register(app))
check_module(api.register(app))
check_module(get.register(app))
diff --git a/atr/blueprints/get.py b/atr/blueprints/admin.py
similarity index 51%
copy from atr/blueprints/get.py
copy to atr/blueprints/admin.py
index 460644c..ad263fe 100644
--- a/atr/blueprints/get.py
+++ b/atr/blueprints/admin.py
@@ -15,83 +15,68 @@
# specific language governing permissions and limitations
# under the License.
-import time
-from collections.abc import Awaitable, Callable
+from collections.abc import Callable
from types import ModuleType
from typing import Any
-import asfquart.auth as auth
import asfquart.base as base
import asfquart.session
import quart
-import atr.log as log
import atr.session as session
+import atr.user as user
-_BLUEPRINT = quart.Blueprint("get_blueprint", __name__)
+_BLUEPRINT_NAME = "admin_blueprint"
+_BLUEPRINT = quart.Blueprint(_BLUEPRINT_NAME, __name__, url_prefix="/admin",
template_folder="../admin/templates")
+
+
+@_BLUEPRINT.before_request
+async def _check_admin_access() -> None:
+ web_session = await asfquart.session.read()
+ if web_session is None:
+ raise base.ASFQuartException("Not authenticated", errorcode=401)
+
+ if web_session.uid not in user.get_admin_users():
+ raise base.ASFQuartException("You are not authorized to access the
admin interface", errorcode=403)
+
+ quart.g.session = session.Committer(web_session)
def register(app: base.QuartApp) -> ModuleType:
- import atr.get as get
+ import atr.admin as admin
app.register_blueprint(_BLUEPRINT)
- return get
+ return admin
-def committer(path: str) -> Callable[[session.CommitterRouteFunction[Any]],
session.RouteFunction[Any]]:
+def get(path: str) -> Callable[[session.CommitterRouteFunction[Any]],
session.RouteFunction[Any]]:
def decorator(func: session.CommitterRouteFunction[Any]) ->
session.RouteFunction[Any]:
async def wrapper(*args: Any, **kwargs: Any) -> Any:
- web_session = await asfquart.session.read()
- if web_session is None:
- raise base.ASFQuartException("Not authenticated",
errorcode=401)
-
- enhanced_session = session.Committer(web_session)
- start_time_ns = time.perf_counter_ns()
- response = await func(enhanced_session, *args, **kwargs)
- end_time_ns = time.perf_counter_ns()
- total_ns = end_time_ns - start_time_ns
- total_ms = total_ns // 1_000_000
-
- # TODO: Make this configurable in config.py
- log.performance(
- "%s %s %s %s %s %s %s",
- "GET",
- path,
- func.__name__,
- "=",
- 0,
- 0,
- total_ms,
- )
-
- return response
+ return await func(quart.g.session, *args, **kwargs)
+ endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
+ wrapper.__annotations__["endpoint"] = _BLUEPRINT_NAME + "." + endpoint
- endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
-
- decorated = auth.require(auth.Requirements.committer)(wrapper)
- _BLUEPRINT.add_url_rule(path, endpoint=endpoint, view_func=decorated,
methods=["GET"])
+ _BLUEPRINT.add_url_rule(path, endpoint=endpoint, view_func=wrapper,
methods=["GET"])
- return decorated
+ return wrapper
return decorator
-def public(path: str) -> Callable[[Callable[..., Awaitable[Any]]],
session.RouteFunction[Any]]:
- def decorator(func: Callable[..., Awaitable[Any]]) ->
session.RouteFunction[Any]:
+def post(path: str) -> Callable[[session.CommitterRouteFunction[Any]],
session.RouteFunction[Any]]:
+ def decorator(func: session.CommitterRouteFunction[Any]) ->
session.RouteFunction[Any]:
async def wrapper(*args: Any, **kwargs: Any) -> Any:
- web_session = await asfquart.session.read()
- enhanced_session = session.Committer(web_session) if web_session
else None
- return await func(enhanced_session, *args, **kwargs)
+ return await func(quart.g.session, *args, **kwargs)
+ endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
+ wrapper.__annotations__["endpoint"] = _BLUEPRINT_NAME + "." + endpoint
- endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
-
- _BLUEPRINT.add_url_rule(path, endpoint=endpoint, view_func=wrapper,
methods=["GET"])
+ _BLUEPRINT.add_url_rule(path, endpoint=endpoint, view_func=wrapper,
methods=["POST"])
return wrapper
diff --git a/atr/blueprints/get.py b/atr/blueprints/get.py
index 460644c..7567d8e 100644
--- a/atr/blueprints/get.py
+++ b/atr/blueprints/get.py
@@ -28,7 +28,8 @@ import quart
import atr.log as log
import atr.session as session
-_BLUEPRINT = quart.Blueprint("get_blueprint", __name__)
+_BLUEPRINT_NAME = "get_blueprint"
+_BLUEPRINT = quart.Blueprint(_BLUEPRINT_NAME, __name__)
def register(app: base.QuartApp) -> ModuleType:
@@ -66,10 +67,10 @@ def committer(path: str) ->
Callable[[session.CommitterRouteFunction[Any]], sess
return response
+ endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
-
- endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
+ wrapper.__annotations__["endpoint"] = _BLUEPRINT_NAME + "." + endpoint
decorated = auth.require(auth.Requirements.committer)(wrapper)
_BLUEPRINT.add_url_rule(path, endpoint=endpoint, view_func=decorated,
methods=["GET"])
@@ -86,10 +87,10 @@ def public(path: str) -> Callable[[Callable[...,
Awaitable[Any]]], session.Route
enhanced_session = session.Committer(web_session) if web_session
else None
return await func(enhanced_session, *args, **kwargs)
+ endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
-
- endpoint = func.__module__.replace(".", "_") + "_" + func.__name__
+ wrapper.__annotations__["endpoint"] = _BLUEPRINT_NAME + "." + endpoint
_BLUEPRINT.add_url_rule(path, endpoint=endpoint, view_func=wrapper,
methods=["GET"])
diff --git a/atr/bps/__init__.py b/atr/bps/__init__.py
index 25f10ae..1fc08c4 100644
--- a/atr/bps/__init__.py
+++ b/atr/bps/__init__.py
@@ -19,8 +19,6 @@ import asfquart.base as base
def register(app: base.QuartApp) -> None:
- import atr.bps.admin.admin as admin
import atr.bps.icons as icons
- app.register_blueprint(admin.admin.BLUEPRINT)
app.register_blueprint(icons.BLUEPRINT)
diff --git a/atr/bps/admin/__init__.py b/atr/bps/admin/__init__.py
deleted file mode 100644
index f3a992e..0000000
--- a/atr/bps/admin/__init__.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# 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.
-
-"""Any routes related to the admin interface of the ATR."""
-
-from typing import Final
-
-import asfquart.auth as auth
-import asfquart.base as base
-import asfquart.session as session
-import quart
-
-import atr.user as user
-
-BLUEPRINT: Final = quart.Blueprint("admin", __name__, url_prefix="/admin",
template_folder="templates")
-
-
[email protected]_request
-async def before_request_func() -> None:
- @auth.require(auth.Requirements.committer)
- async def check_logged_in() -> None:
- web_session = await session.read()
- if web_session is None:
- raise base.ASFQuartException("Not authenticated", errorcode=401)
-
- if web_session.uid not in user.get_admin_users():
- raise base.ASFQuartException("You are not authorized to access the
admin interface", errorcode=403)
-
- await check_logged_in()
diff --git a/atr/principal.py b/atr/principal.py
index 72cc894..73d3829 100644
--- a/atr/principal.py
+++ b/atr/principal.py
@@ -22,12 +22,14 @@ import re
import time
from typing import Any, Final
-import asfquart.session as session
+import asfquart.session
+# from attr import asdict
import atr.config as config
import atr.ldap as ldap
import atr.log as log
import atr.route as route
+import atr.session as session
import atr.util as util
LDAP_CHAIRS_BASE = "cn=pmc-chairs,ou=groups,ou=services,dc=apache,dc=org"
@@ -59,7 +61,7 @@ class ArgumentNoneType:
ArgumentNone = ArgumentNoneType()
-type UID = route.CommitterSession | str | None | ArgumentNoneType
+type UID = route.CommitterSession | session.Committer | str | None |
ArgumentNoneType
def attr_to_list(attr):
@@ -256,10 +258,10 @@ class AuthoriserASFQuart:
def member_of_and_participant_of(self, asf_uid: str) ->
tuple[frozenset[str], frozenset[str]]:
return self.__cache.member_of[asf_uid],
self.__cache.participant_of[asf_uid]
- async def cache_refresh(self, asf_uid: str, asfquart_session:
session.ClientSession) -> None:
+ async def cache_refresh(self, asf_uid: str, asfquart_session:
asfquart.session.ClientSession) -> None:
if not self.__cache.outdated(asf_uid):
return
- if not isinstance(asfquart_session, session.ClientSession):
+ if not isinstance(asfquart_session, asfquart.session.ClientSession):
# Defense in depth runtime check, already validated by the type
checker
raise AuthenticationError("ASFQuart session is not a
ClientSession")
@@ -352,12 +354,12 @@ class AsyncObject:
class Authorisation(AsyncObject):
async def __init__(self, asf_uid: UID = ArgumentNone):
match asf_uid:
- case ArgumentNoneType() | route.CommitterSession():
+ case ArgumentNoneType() | route.CommitterSession() |
session.Committer():
match asf_uid:
- case route.CommitterSession():
+ case route.CommitterSession() | session.Committer():
asfquart_session = asf_uid._session
case _:
- asfquart_session = await session.read()
+ asfquart_session = await asfquart.session.read()
# asfquart_session = await session.read()
if asfquart_session is None:
raise AuthenticationError("No ASFQuart session found")
diff --git a/atr/server.py b/atr/server.py
index b827078..0251064 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -128,11 +128,13 @@ def app_setup_context(app: base.QuartApp) -> None:
@app.context_processor
async def app_wide() -> dict[str, Any]:
+ import atr.admin as admin
import atr.metadata as metadata
import atr.routes as routes
import atr.routes.mapping as mapping
return {
+ "admin": admin,
"as_url": util.as_url,
"commit": metadata.commit,
"current_user": await asfquart.session.read(),
diff --git a/atr/templates/delete-committee-keys.html
b/atr/templates/delete-committee-keys.html
index 3d33141..61e7611 100644
--- a/atr/templates/delete-committee-keys.html
+++ b/atr/templates/delete-committee-keys.html
@@ -11,7 +11,7 @@
<h1 class="mb-4">Delete all keys for a committee</h1>
<form method="post"
- action="{{ url_for('admin.admin_delete_committee_keys') }}"
+ action="{{ as_url(admin.delete_committee_keys_post) }}"
class="atr-canary py-4 px-5 border rounded">
{{ form.csrf_token }}
diff --git a/atr/templates/includes/sidebar.html
b/atr/templates/includes/sidebar.html
index 55ac2fc..ef2a63a 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -138,83 +138,83 @@
<ul>
<li>
<i class="bi bi-list-ul"></i>
- <a href="{{ url_for('admin.admin_all_releases') }}"
- {% if request.endpoint == 'admin.admin_all_releases'
%}class="active"{% endif %}>All releases</a>
+ <a href="{{ as_url(admin.all_releases) }}"
+ {% if request.endpoint == 'atr_admin_all_releases'
%}class="active"{% endif %}>All releases</a>
</li>
<li>
<i class="bi bi-person-plus"></i>
- <a href="{{ url_for('admin.admin_browse_as') }}"
- {% if request.endpoint == 'admin.admin_browse_as'
%}class="active"{% endif %}>Browse as user</a>
+ <a href="{{ as_url(admin.browse_as_get) }}"
+ {% if request.endpoint == 'atr_admin_browse_as_get'
%}class="active"{% endif %}>Browse as user</a>
</li>
<li>
<i class="bi bi-arrow-repeat"></i>
- <a href="{{ url_for('admin.admin_consistency') }}"
- {% if request.endpoint == 'admin.admin_consistency'
%}class="active"{% endif %}>Consistency</a>
+ <a href="{{ as_url(admin.consistency) }}"
+ {% if request.endpoint == 'atr_admin_consistency'
%}class="active"{% endif %}>Consistency</a>
</li>
<li>
<i class="bi bi-database"></i>
- <a href="{{ url_for('admin.admin_data') }}"
- {% if request.endpoint == 'admin.admin_data' %}class="active"{%
endif %}>Browse database</a>
+ <a href="{{ as_url(admin.data) }}"
+ {% if request.endpoint == 'atr_admin_data' %}class="active"{%
endif %}>Browse database</a>
</li>
<li>
<i class="bi bi-trash"></i>
- <a href="{{ url_for('admin.admin_delete_committee_keys') }}"
- {% if request.endpoint == 'admin.admin_delete_committee_keys'
%}class="active"{% endif %}>Delete committee keys</a>
+ <a href="{{ as_url(admin.delete_committee_keys_get) }}"
+ {% if request.endpoint == 'atr_admin_delete_committee_keys_get'
%}class="active"{% endif %}>Delete committee keys</a>
</li>
<li>
<i class="bi bi-trash"></i>
- <a href="{{ url_for('admin.admin_delete_release') }}"
- {% if request.endpoint == 'admin.admin_delete_release'
%}class="active"{% endif %}>Delete release</a>
+ <a href="{{ as_url(admin.delete_release_get) }}"
+ {% if request.endpoint == 'atr_admin_delete_release_get'
%}class="active"{% endif %}>Delete release</a>
</li>
<li>
<i class="bi bi-gear"></i>
- <a href="{{ url_for('admin.admin_env') }}"
- {% if request.endpoint == 'admin.admin_env' %}class="active"{%
endif %}>Environment</a>
+ <a href="{{ as_url(admin.env) }}"
+ {% if request.endpoint == 'atr_admin_env' %}class="active"{%
endif %}>Environment</a>
</li>
<li>
<i class="bi bi-key"></i>
- <a href="{{ url_for('admin.admin_keys_check') }}"
- {% if request.endpoint == 'admin.admin_keys_check'
%}class="active"{% endif %}>Keys check</a>
+ <a href="{{ as_url(admin.keys_check_get) }}"
+ {% if request.endpoint == 'atr_admin_keys_check_get'
%}class="active"{% endif %}>Keys check</a>
</li>
<li>
<i class="bi bi-key"></i>
- <a href="{{ url_for('admin.admin_keys_regenerate_all') }}"
- {% if request.endpoint == 'admin.admin_keys_regenerate_all'
%}class="active"{% endif %}>Regenerate all keys</a>
+ <a href="{{ as_url(admin.keys_regenerate_all_get) }}"
+ {% if request.endpoint == 'atr_admin_keys_regenerate_all_get'
%}class="active"{% endif %}>Regenerate all keys</a>
</li>
<li>
<i class="bi bi-key"></i>
- <a href="{{ url_for('admin.admin_keys_update') }}"
- {% if request.endpoint == 'admin.admin_keys_update'
%}class="active"{% endif %}>Update keys</a>
+ <a href="{{ as_url(admin.keys_update_get) }}"
+ {% if request.endpoint == 'atr_admin_keys_update_get'
%}class="active"{% endif %}>Update keys</a>
</li>
<li>
<i class="bi bi-person-plus"></i>
- <a href="{{ url_for('admin.admin_ldap') }}"
- {% if request.endpoint == 'admin.admin_ldap' %}class="active"{%
endif %}>LDAP search</a>
+ <a href="{{ as_url(admin.ldap_get) }}"
+ {% if request.endpoint == 'atr_admin_ldap_get'
%}class="active"{% endif %}>LDAP search</a>
</li>
<li>
<i class="bi bi-speedometer2"></i>
- <a href="{{ url_for('admin.admin_performance') }}"
- {% if request.endpoint == 'admin.admin_performance'
%}class="active"{% endif %}>Page performance</a>
+ <a href="{{ as_url(admin.performance) }}"
+ {% if request.endpoint == 'atr_admin_performance'
%}class="active"{% endif %}>Page performance</a>
</li>
<li>
<i class="bi bi-arrow-repeat"></i>
- <a href="{{ url_for('admin.admin_projects_update') }}"
- {% if request.endpoint == 'admin.admin_projects_update'
%}class="active"{% endif %}>Update projects</a>
+ <a href="{{ as_url(admin.projects_update_get) }}"
+ {% if request.endpoint == 'atr_admin_projects_update_get'
%}class="active"{% endif %}>Update projects</a>
</li>
<li>
<i class="bi bi-list-task"></i>
- <a href="{{ url_for('admin.admin_tasks') }}"
- {% if request.endpoint == 'admin.admin_tasks'
%}class="active"{% endif %}>Background tasks</a>
+ <a href="{{ as_url(admin.tasks_) }}"
+ {% if request.endpoint == 'atr_admin_tasks_' %}class="active"{%
endif %}>Background tasks</a>
</li>
<li>
<i class="bi bi-person-badge"></i>
- <a href="{{ url_for('admin.admin_toggle_admin_view_page') }}"
- {% if request.endpoint == 'admin.admin_toggle_admin_view_page'
%}class="active"{% endif %}>Toggle admin view</a>
+ <a href="{{ as_url(admin.toggle_view_get) }}"
+ {% if request.endpoint == 'atr_admin_toggle_view_get'
%}class="active"{% endif %}>Toggle admin view</a>
</li>
<li>
<i class="bi bi-arrow-repeat"></i>
- <a href="{{ url_for('admin.admin_validate') }}"
- {% if request.endpoint == 'admin.admin_validate'
%}class="active"{% endif %}>Validate</a>
+ <a href="{{ as_url(admin.validate_) }}"
+ {% if request.endpoint == 'atr_admin_validate_'
%}class="active"{% endif %}>Validate</a>
</li>
</ul>
{% endif %}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]