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 4ca39cd  Use CSRF protection consistently throughout
4ca39cd is described below

commit 4ca39cdeec7677f1fa37ff1913483e4ea84824f8
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon May 12 19:49:44 2025 +0100

    Use CSRF protection consistently throughout
---
 atr/blueprints/admin/admin.py                      | 89 +++-------------------
 atr/blueprints/admin/templates/delete-release.html |  2 +-
 .../admin/templates/toggle-admin-view.html         | 45 +++++++++++
 .../admin/templates/update-projects.html           |  7 ++
 atr/config.py                                      |  3 +
 atr/routes/compose.py                              |  2 +
 atr/routes/draft.py                                |  6 +-
 atr/routes/keys.py                                 |  1 +
 atr/routes/projects.py                             |  7 +-
 atr/routes/report.py                               |  1 +
 atr/routes/revisions.py                            |  3 +
 atr/server.py                                      | 10 +--
 atr/templates/announce-selected.html               |  1 +
 atr/templates/check-selected-candidate-forms.html  |  4 +-
 atr/templates/check-selected-path-table.html       |  2 +
 atr/templates/draft-tools.html                     |  6 ++
 atr/templates/finish-selected.html                 |  1 +
 atr/templates/includes/sidebar.html                | 21 ++---
 atr/templates/keys-add.html                        |  2 +-
 atr/templates/keys-review.html                     |  3 +
 atr/templates/keys-ssh-add.html                    |  3 +-
 atr/templates/keys-upload.html                     |  3 +-
 atr/templates/macros/dialog.html                   |  2 +
 atr/templates/project-view.html                    |  2 +
 atr/templates/projects.html                        |  2 +
 atr/templates/release-policy-form.html             |  3 +-
 atr/templates/report-selected-path.html            |  2 +
 atr/templates/revisions-selected.html              |  2 +
 atr/templates/start-selected.html                  |  2 -
 atr/templates/upload-selected.html                 |  4 +-
 atr/templates/voting-selected-revision.html        |  4 +-
 atr/util.py                                        | 10 +++
 32 files changed, 139 insertions(+), 116 deletions(-)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 141e959..35f6566 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -16,12 +16,10 @@
 # under the License.
 
 import collections
-import datetime
 import logging
 import os
 import pathlib
 import statistics
-import uuid
 from collections.abc import Callable, Mapping
 from typing import Any, Final
 
@@ -165,28 +163,6 @@ async def admin_env() -> quart.wrappers.response.Response:
     return quart.Response("\n".join(env_vars), mimetype="text/plain")
 
 
[email protected]("/keys/delete-all")
-async def admin_keys_delete_all() -> str:
-    """Debug endpoint to delete all of a user's keys."""
-    web_session = await session.read()
-    if web_session is None:
-        raise base.ASFQuartException("Not authenticated", errorcode=401)
-    uid = util.unwrap(web_session.uid)
-
-    async with db.session() as data:
-        async with data.begin():
-            # Get all keys for the user
-            # TODO: Use session.apache_uid instead of session.uid?
-            keys = await data.public_signing_key(apache_uid=uid).all()
-            count = len(keys)
-
-            # Delete all keys
-            for key in keys:
-                await data.delete(key)
-
-        return f"Deleted {count} keys"
-
-
 @admin.BLUEPRINT.route("/performance")
 async def admin_performance() -> str:
     """Display performance statistics for all routes."""
@@ -291,7 +267,8 @@ async def admin_projects_update() -> str | 
response.Response | tuple[Mapping[str
             }, 200
 
     # For GET requests, show the update form
-    return await quart.render_template("update-projects.html")
+    empty_form = await util.EmptyForm.create_form()
+    return await quart.render_template("update-projects.html", 
empty_form=empty_form)
 
 
 @admin.BLUEPRINT.route("/releases")
@@ -307,65 +284,17 @@ async def admin_tasks() -> str:
     return await quart.render_template("tasks.html")
 
 
[email protected]("/test-kv")
-async def admin_test_kv() -> str:
-    """Test route for writing and reading from the TextValue KV store."""
-    test_ns = "kv_test"
-    test_key = str(uuid.uuid4())
-    test_value = f"Test value set at {datetime.datetime.now(datetime.UTC)}"
-    message: str
-
-    try:
-        async with db.session() as data:
-            existing = await data.text_value(ns=test_ns, key=test_key).get()
-            if existing:
-                existing.value = test_value
-                data.add(existing)
-            else:
-                new_entry = models.TextValue(ns=test_ns, key=test_key, 
value=test_value)
-                data.add(new_entry)
-            await data.commit()
-            _LOGGER.info(f"Text value test: Wrote {test_ns}/{test_key} = 
{test_value}")
-
-        async with db.session() as data:
-            read_back = await data.text_value(ns=test_ns, key=test_key).get()
-            if read_back and (read_back.value == test_value):
-                message = f"<p class='page-success'>Test SUCCESS: Wrote/read 
ok (ns='{test_ns}', key='{test_key}')</p>"
-                _LOGGER.info("Text value test SUCCESS")
-            elif read_back:
-                message = (
-                    f"<p class='page-error'>Test FAILED: Read back wrong 
value!</p>"
-                    f"<p>Expected: '{test_value}'</p>"
-                    f"<p>Got: '{read_back.value}'</p>"
-                )
-                _LOGGER.error(
-                    f"Text value test FAILED: Read back wrong value! 
Expected='{test_value}', got='{read_back.value}'"
-                )
-            else:
-                message = f"<p class='page-success'>Test SUCCESS: Wrote/read 
ok (ns='{test_ns}', key='{test_key}')</p>"
-                _LOGGER.info("Text value test SUCCESS")
-
-    except Exception as e:
-        message = f"<p class='page-error'>Test FAILED: Exception occurred - 
{e!s}</p>"
-        _LOGGER.exception("Text value test exception")
-
-    return f"""<!DOCTYPE html>
-<html>
-<head><title>Text value test result</title></head>
-<style>
-.page-error {{ color: red; }}
-.page-success {{ color: green; }}
-</style>
-<body>
-<h1>Text value test result</h1>
-{message}
-</body>
-</html>
-"""
[email protected]("/toggle-view", methods=["GET"])
+async def admin_toggle_admin_view_page() -> str:
+    """Display the page with a button to toggle between admin and user 
views."""
+    empty_form = await util.EmptyForm.create_form()
+    return await quart.render_template("toggle-admin-view.html", 
empty_form=empty_form)
 
 
 @admin.BLUEPRINT.route("/toggle-admin-view", methods=["POST"])
 async def admin_toggle_view() -> response.Response:
+    await util.validate_empty_form()
+
     web_session = await session.read()
     if web_session is None:
         # For the type checker
diff --git a/atr/blueprints/admin/templates/delete-release.html 
b/atr/blueprints/admin/templates/delete-release.html
index 570fa60..50ea957 100644
--- a/atr/blueprints/admin/templates/delete-release.html
+++ b/atr/blueprints/admin/templates/delete-release.html
@@ -16,7 +16,7 @@
   </div>
 
   <form method="post" novalidate>
-    {{ form.csrf_token }}
+    {{ form.hidden_tag() }}
 
     <div class="mb-3">
       <label class="form-label">Select releases to delete:</label>
diff --git a/atr/blueprints/admin/templates/toggle-admin-view.html 
b/atr/blueprints/admin/templates/toggle-admin-view.html
new file mode 100644
index 0000000..725b731
--- /dev/null
+++ b/atr/blueprints/admin/templates/toggle-admin-view.html
@@ -0,0 +1,45 @@
+{% extends "layouts/base-admin.html" %}
+
+{% block title %}Toggle admin view{% endblock title %}
+
+{% block description %}
+  Switch between administrator and regular user views.
+{% endblock description %}
+
+{% block content %}
+  <h1>Toggle admin view</h1>
+
+  <p class="mb-4">
+    Use this page to switch between viewing the site as an administrator or as 
a regular user.
+    This is helpful for testing permissions and user experience from different 
perspectives.
+  </p>
+
+  {% if current_user and is_admin_fn(current_user.uid) %}
+    <form action="{{ url_for('admin.admin_toggle_view') }}" method="post" 
class="mb-4">
+      {{ empty_form.hidden_tag() }}
+      <button type="submit" class="btn btn-primary">
+        {% if not is_viewing_as_admin_fn(current_user.uid) %}
+          <i class="fa-solid fa-user-shield"></i> Switch to admin view
+        {% else %}
+          <i class="fa-solid fa-user-ninja"></i> Switch to user view
+        {% endif %}
+      </button>
+    </form>
+
+    <div class="alert alert-info" role="alert">
+      Current view mode:
+      <strong>
+        {% if is_viewing_as_admin_fn(current_user.uid) %}
+          Administrator
+        {% else %}
+          Regular user
+        {% endif %}
+      </strong>
+    </div>
+
+  {% else %}
+    <div class="alert alert-warning" role="alert">
+      This function is only available to administrators.
+    </div>
+  {% endif %}
+{% endblock content %}
diff --git a/atr/blueprints/admin/templates/update-projects.html 
b/atr/blueprints/admin/templates/update-projects.html
index e6ae028..c997001 100644
--- a/atr/blueprints/admin/templates/update-projects.html
+++ b/atr/blueprints/admin/templates/update-projects.html
@@ -91,6 +91,8 @@
   <div id="status"></div>
 
   <form action="javascript:submitForm().then(_ => { return false; })">
+    {{ empty_form.hidden_tag() }}
+
     <button type="submit" id="submitButton">Update projects</button>
   </form>
 
@@ -105,9 +107,14 @@
         statusElement.firstChild.remove();
       }
 
+      const csrfToken = 
document.querySelector("input[name='csrf_token']").value;
+
       try {
           const response = await fetch(window.location.href, {
               method: "POST",
+              headers: {
+                  "X-CSRFToken": csrfToken
+              }
           });
 
           if (!response.ok) {
diff --git a/atr/config.py b/atr/config.py
index bdc0084..a462348 100644
--- a/atr/config.py
+++ b/atr/config.py
@@ -17,6 +17,7 @@
 
 import enum
 import os
+import secrets
 from typing import Final
 
 import decouple
@@ -34,6 +35,8 @@ class AppConfig:
     DEBUG = False
     TEMPLATES_AUTO_RELOAD = False
     USE_BLOCKBUSTER = False
+    SECRET_KEY = decouple.config("SECRET_KEY", default=secrets.token_hex(128 
// 8))
+    WTF_CSRF_ENABLED = decouple.config("WTF_CSRF_ENABLED", default=True, 
cast=bool)
     FINISHED_STORAGE_DIR = os.path.join(STATE_DIR, "finished")
     UNFINISHED_STORAGE_DIR = os.path.join(STATE_DIR, "unfinished")
     SQLITE_DB_PATH = decouple.config("SQLITE_DB_PATH", default="atr.db")
diff --git a/atr/routes/compose.py b/atr/routes/compose.py
index c79cf1a..771ae16 100644
--- a/atr/routes/compose.py
+++ b/atr/routes/compose.py
@@ -67,6 +67,7 @@ async def check(
     delete_draft_form = await draft.DeleteForm.create_form()
     delete_file_form = await draft.DeleteFileForm.create_form()
     resolve_form = await resolve.ResolveForm.create_form()
+    empty_form = await util.EmptyForm.create_form()
     vote_task_warnings = _warnings_from_vote_result(vote_task)
 
     return await quart.render_template(
@@ -93,6 +94,7 @@ async def check(
         vote_task=vote_task,
         archive_url=archive_url,
         vote_task_warnings=vote_task_warnings,
+        empty_form=empty_form,
     )
 
 
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index e0889a5..51b1dd7 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -191,6 +191,7 @@ async def fresh(session: routes.CommitterSession, 
project_name: str, version_nam
     # Admin only button, but it's okay if users find and use this manually
     await session.check_access(project_name)
 
+    await util.validate_empty_form()
     # Restart checks by creating a new identical draft revision
     # This doesn't make sense unless the checks themselves have been updated
     # Therefore we only show the button for this to admins
@@ -216,7 +217,8 @@ async def hashgen(
     await session.check_access(project_name)
 
     # Get the hash type from the form data
-    # This is just a button, so we don't make a whole form validation schema 
for it
+    # TODO: This is not truly empty, so make a form object for this
+    await util.validate_empty_form()
     form = await quart.request.form
     hash_type = form.get("hash_type")
     if hash_type not in {"sha256", "sha512"}:
@@ -275,6 +277,7 @@ async def sbomgen(
     """Generate a CycloneDX SBOM file for a candidate draft file, creating a 
new revision."""
     await session.check_access(project_name)
 
+    await util.validate_empty_form()
     rel_path = pathlib.Path(file_path)
 
     # Check that the file is a .tar.gz archive before creating a revision
@@ -427,6 +430,7 @@ async def tools(session: routes.CommitterSession, 
project_name: str, version_nam
         file_data=file_data,
         release=release,
         format_file_size=util.format_file_size,
+        empty_form=await util.EmptyForm.create_form(),
     )
 
 
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index ed04911..724290b 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -162,6 +162,7 @@ async def import_selected_revision(
 ) -> response.Response:
     await session.check_access(project_name)
 
+    await util.validate_empty_form()
     release = await session.release(project_name, version_name, 
with_committee=True)
     keys_path = util.release_directory(release) / "KEYS"
     async with aiofiles.open(keys_path, encoding="utf-8") as f:
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 541c2f0..672bbd0 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -107,6 +107,8 @@ async def add_project(session: routes.CommitterSession, 
project_name: str) -> re
 @routes.committer("/project/delete", methods=["POST"])
 async def delete(session: routes.CommitterSession) -> response.Response:
     """Delete a project created by the user."""
+    # TODO: This is not truly empty, so make a form object for this
+    await util.validate_empty_form()
     form_data = await quart.request.form
     project_name = form_data.get("project_name")
     if not project_name:
@@ -149,7 +151,9 @@ async def projects() -> str:
     """Main project directory page."""
     async with db.session() as data:
         projects = await 
data.project(_committee=True).order_by(models.Project.full_name).all()
-        return await quart.render_template("projects.html", projects=projects)
+        return await quart.render_template(
+            "projects.html", projects=projects, empty_form=await 
util.EmptyForm.create_form()
+        )
 
 
 @routes.committer("/projects/<project_name>/release-policy/add", 
methods=["GET", "POST"])
@@ -261,6 +265,7 @@ async def view(name: str) -> str:
             full_releases=await project.full_releases,
             number_of_release_files=util.number_of_release_files,
             now=datetime.datetime.now(datetime.UTC),
+            empty_form=await util.EmptyForm.create_form(),
         )
 
 
diff --git a/atr/routes/report.py b/atr/routes/report.py
index 4c78d33..1b6b980 100644
--- a/atr/routes/report.py
+++ b/atr/routes/report.py
@@ -93,4 +93,5 @@ async def selected_path(session: routes.CommitterSession, 
project_name: str, ver
         primary_results=primary_results_list,
         member_results=member_results_list,
         format_file_size=util.format_file_size,
+        empty_form=await util.EmptyForm.create_form(),
     )
diff --git a/atr/routes/revisions.py b/atr/routes/revisions.py
index c18310b..6cef6b7 100644
--- a/atr/routes/revisions.py
+++ b/atr/routes/revisions.py
@@ -106,6 +106,7 @@ async def selected(session: routes.CommitterSession, 
project_name: str, version_
         phase_key=phase_key,
         revision_history=list(reversed(revision_history)),
         current_revision_name=current_revision_name,
+        empty_form=await util.EmptyForm.create_form(),
     )
 
 
@@ -114,6 +115,8 @@ async def selected_post(session: routes.CommitterSession, 
project_name: str, ver
     """Set a specific revision as the latest for a candidate draft or release 
preview."""
     await session.check_access(project_name)
 
+    # TODO: This is not truly empty, so make a form object for this
+    await util.validate_empty_form()
     form_data = await quart.request.form
     revision_name = form_data.get("revision_name")
     if not revision_name:
diff --git a/atr/server.py b/atr/server.py
index e47573c..190ce51 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -30,6 +30,7 @@ import asfquart.session
 import blockbuster
 import quart
 import quart_schema
+import quart_wtf
 import rich.logging as rich_logging
 import werkzeug.routing as routing
 
@@ -174,19 +175,16 @@ def app_setup_logging(app: base.QuartApp, config_mode: 
config.Mode, app_config:
 
 def create_app(app_config: type[config.AppConfig]) -> base.QuartApp:
     """Create and configure the application."""
+    config_mode = config.get_mode()
     app_dirs_setup(app_config)
-
     app = app_create_base(app_config)
-    app_setup_api_docs(app)
 
+    app_setup_api_docs(app)
+    quart_wtf.CSRFProtect(app)
     db.init_database(app)
     register_routes(app)
     blueprints.register(app)
-
     filters.register_filters(app)
-
-    config_mode = config.get_mode()
-
     app_setup_context(app)
     app_setup_lifecycle(app)
     app_setup_logging(app, config_mode, app_config)
diff --git a/atr/templates/announce-selected.html 
b/atr/templates/announce-selected.html
index 33958a0..ef66d65 100644
--- a/atr/templates/announce-selected.html
+++ b/atr/templates/announce-selected.html
@@ -70,6 +70,7 @@
         action="{{ as_url(routes.announce.selected_post, 
project_name=release.project.name, version_name=release.version) }}"
         class="atr-canary py-4 px-5 mb-4 border rounded">
     {{ announce_form.hidden_tag() }}
+
     <div class="row mb-3 pb-3 border-bottom">
       <div class="col-md-3 text-md-end fw-medium">{{ 
announce_form.mailing_list.label }}</div>
       <div class="col-md-9">
diff --git a/atr/templates/check-selected-candidate-forms.html 
b/atr/templates/check-selected-candidate-forms.html
index f7e0848..fc333f3 100644
--- a/atr/templates/check-selected-candidate-forms.html
+++ b/atr/templates/check-selected-candidate-forms.html
@@ -13,6 +13,7 @@
       action="{{ as_url(routes.vote.selected_post, project_name=project_name, 
version_name=version_name) }}"
       class="atr-canary py-4 px-5 mb-4 border rounded">
   {{ form.hidden_tag() }}
+
   <div class="row mb-3 pb-3 border-bottom">
     <label class="col-md-3 col-form-label text-md-end">{{ 
form.vote_value.label.text }}:</label>
     <div class="col-md-9">
@@ -56,8 +57,9 @@
       action="{{ as_url(routes.resolve.selected_post, 
project_name=release.project.name, version_name=release.version) }}"
       class="atr-canary py-4 px-5"
       novalidate>
+  {{ resolve_form.hidden_tag() }}
+
   <input type="hidden" name="candidate_name" value="{{ release.name }}" />
-  {{ resolve_form.csrf_token }}
 
   <div class="mb-3 pb-3 row border-bottom">
     <label class="col-sm-3 col-form-label text-sm-end fw-semibold">{{ 
resolve_form.vote_result.label.text }}:</label>
diff --git a/atr/templates/check-selected-path-table.html 
b/atr/templates/check-selected-path-table.html
index b60ded9..ebe5bed 100644
--- a/atr/templates/check-selected-path-table.html
+++ b/atr/templates/check-selected-path-table.html
@@ -60,6 +60,8 @@
                 <form method="post"
                       action="{{ as_url(routes.keys.import_selected_revision, 
project_name=project_name, version_name=version_name) }}"
                       class="d-inline mb-0">
+                  {{ empty_form.hidden_tag() }}
+
                   <button type="submit" class="btn btn-sm 
btn-outline-primary">Import keys</button>
                 </form>
               {% endif %}
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index a17518f..f5ce2bb 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -36,11 +36,15 @@
   <div class="d-flex gap-2 mb-4">
     <form method="post"
           action="{{ as_url(routes.draft.hashgen, project_name=project_name, 
version_name=version_name, file_path=file_path) }}">
+      {{ empty_form.hidden_tag() }}
+
       <input type="hidden" name="hash_type" value="sha256" />
       <button type="submit" class="btn btn-outline-secondary">Generate 
SHA256</button>
     </form>
     <form method="post"
           action="{{ as_url(routes.draft.hashgen, project_name=project_name, 
version_name=version_name, file_path=file_path) }}">
+      {{ empty_form.hidden_tag() }}
+
       <input type="hidden" name="hash_type" value="sha512" />
       <button type="submit" class="btn btn-outline-secondary">Generate 
SHA512</button>
     </form>
@@ -51,6 +55,8 @@
     <p>Generate a CycloneDX Software Bill of Materials (SBOM) file for this 
artifact.</p>
     <form method="post"
           action="{{ as_url(routes.draft.sbomgen, project_name=project_name, 
version_name=version_name, file_path=file_path) }}">
+      {{ empty_form.hidden_tag() }}
+
       <button type="submit" class="btn btn-outline-secondary">Generate 
CycloneDX SBOM (.cdx.json)</button>
     </form>
   {% endif %}
diff --git a/atr/templates/finish-selected.html 
b/atr/templates/finish-selected.html
index 0b21267..88caf9f 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -82,6 +82,7 @@
       <div class="card-body">
         <form method="post" class="atr-canary">
           {{ form.hidden_tag() }}
+
           <div class="mb-3">
             {{ form.source_file.label(class="form-label") }}
             {{ form.source_file(class="form-select form-select-sm 
font-monospace") }}
diff --git a/atr/templates/includes/sidebar.html 
b/atr/templates/includes/sidebar.html
index 6891536..6c808d6 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -129,24 +129,13 @@
             <a href="{{ url_for('admin.admin_delete_release') }}"
                {% if request.endpoint == 'admin.admin_delete_release' 
%}class="active"{% endif %}>Delete release</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>
+          </li>
         </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/templates/keys-add.html b/atr/templates/keys-add.html
index ee581a6..2bed0c2 100644
--- a/atr/templates/keys-add.html
+++ b/atr/templates/keys-add.html
@@ -23,7 +23,7 @@
           class="atr-canary py-4 px-5"
           action="{{ as_url(routes.keys.add) }}"
           novalidate>
-      {{ form.hidden_tag() if form.hidden_tag }}
+      {{ form.hidden_tag() }}
 
       <div class="mb-4">
         <div class="row mb-3 pb-3 border-bottom">
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
index 6103f92..3765e53 100644
--- a/atr/templates/keys-review.html
+++ b/atr/templates/keys-review.html
@@ -103,6 +103,7 @@
                   class="mt-3"
                   onsubmit="return confirm('Are you sure you want to delete 
this GPG key?');">
               {{ delete_form.hidden_tag() }}
+
               <input type="hidden" name="fingerprint" value="{{ 
key.fingerprint }}" />
               {{ delete_form.submit(class_='btn btn-danger', value='Delete 
key') }}
             </form>
@@ -145,6 +146,7 @@
                   class="mt-3"
                   onsubmit="return confirm('Are you sure you want to delete 
this SSH key?');">
               {{ delete_form.hidden_tag() }}
+
               <input type="hidden" name="fingerprint" value="{{ 
key.fingerprint }}" />
               {{ delete_form.submit(class_='btn btn-danger', value='Delete 
key') }}
             </form>
@@ -192,6 +194,7 @@
             action="{{ as_url(routes.keys.update_committee_keys, 
committee_name=committee.name) }}"
             class="mb-4 d-inline-block">
         {{ update_committee_keys_form.hidden_tag() }}
+
         {{ update_committee_keys_form.submit(class_='btn btn-sm 
btn-outline-secondary') }}
       </form>
     {% else %}
diff --git a/atr/templates/keys-ssh-add.html b/atr/templates/keys-ssh-add.html
index fdb8091..a19e426 100644
--- a/atr/templates/keys-ssh-add.html
+++ b/atr/templates/keys-ssh-add.html
@@ -32,7 +32,8 @@
   {% endif %}
 
   <form method="post" class="atr-canary">
-    {{ form.csrf_token }}
+    {{ form.hidden_tag() }}
+
     <div class="mb-4">
       <div class="mb-3">
         <label for="key" class="form-label">SSH public key:</label>
diff --git a/atr/templates/keys-upload.html b/atr/templates/keys-upload.html
index 681bdd0..53f2b9c 100644
--- a/atr/templates/keys-upload.html
+++ b/atr/templates/keys-upload.html
@@ -153,8 +153,7 @@
   <form method="post"
         class="atr-canary py-4 px-5"
         enctype="multipart/form-data">
-    {# {{ form.csrf_token }} #}
-    {{ form.hidden_tag() if form.hidden_tag }}
+    {{ form.hidden_tag() }}
 
     <div class="mb-4">
       <div class="row mb-3 pb-3 border-bottom">
diff --git a/atr/templates/macros/dialog.html b/atr/templates/macros/dialog.html
index fd45628..b859931 100644
--- a/atr/templates/macros/dialog.html
+++ b/atr/templates/macros/dialog.html
@@ -23,6 +23,7 @@
           </p>
           <form method="post" action="{{ action }}">
             {{ form.hidden_tag() }}
+
             {{ form[field_name](value_=id, hidden=True) }}
             <div class="mb-3">
               <label for="confirm_delete_{{ element_id }}" class="form-label">
@@ -69,6 +70,7 @@
           </p>
           <form method="post" action="{{ action }}">
             {{ form.hidden_tag() }}
+
             {{ form[field_name](value_=id, hidden=True) }}
             {{ form.submit(class_="btn btn-danger", id_="delete-button-" + 
element_id) }}
           </form>
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index 305a452..e114661 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -261,6 +261,8 @@
               action="{{ as_url(routes.projects.delete) }}"
               class="d-inline-block m-0"
               onsubmit="return confirm('Are you sure you want to delete the 
project \'{{ project.display_name }}\'? This cannot be undone.');">
+          {{ empty_form.hidden_tag() }}
+
           <input type="hidden" name="project_name" value="{{ project.name }}" 
/>
           <button type="submit"
                   class="btn btn-sm btn-outline-danger"
diff --git a/atr/templates/projects.html b/atr/templates/projects.html
index 236cf1a..40eb4b5 100644
--- a/atr/templates/projects.html
+++ b/atr/templates/projects.html
@@ -83,6 +83,8 @@
                       action="{{ as_url(routes.projects.delete) }}"
                       class="d-inline-block m-0"
                       onsubmit="return confirm('Are you sure you want to 
delete the project \'{{ project.display_name }}\'? This cannot be undone.');">
+                  {{ empty_form.hidden_tag() }}
+
                   <input type="hidden" name="project_name" value="{{ 
project.name }}" />
                   <button type="submit"
                           class="btn btn-sm btn-outline-danger"
diff --git a/atr/templates/release-policy-form.html 
b/atr/templates/release-policy-form.html
index f03d808..b170197 100644
--- a/atr/templates/release-policy-form.html
+++ b/atr/templates/release-policy-form.html
@@ -3,8 +3,9 @@
       enctype="multipart/form-data"
       class="atr-canary py-4 px-5"
       novalidate>
-  <input type="hidden" name="form_type" value="single" />
   {{ form.hidden_tag() }}
+
+  <input type="hidden" name="form_type" value="single" />
   <div class="mb-3 pb-3 row border-bottom">
     <label for="project_name_text" class="col-sm-3 col-form-label 
text-sm-end">Project:</label>
     <div class="col-sm-8">
diff --git a/atr/templates/report-selected-path.html 
b/atr/templates/report-selected-path.html
index 63af4e5..78fbeeb 100644
--- a/atr/templates/report-selected-path.html
+++ b/atr/templates/report-selected-path.html
@@ -87,6 +87,8 @@
       <form method="post"
             action="{{ as_url(routes.draft.fresh, 
project_name=release.project.name, version_name=release.version) }}"
             class="mb-0">
+        {{ empty_form.hidden_tag() }}
+
         <button type="submit" class="btn btn-primary">Restart all 
checks</button>
       </form>
     {% endif %}
diff --git a/atr/templates/revisions-selected.html 
b/atr/templates/revisions-selected.html
index 079503d..aa5796d 100644
--- a/atr/templates/revisions-selected.html
+++ b/atr/templates/revisions-selected.html
@@ -110,6 +110,8 @@
             <div class="mt-3">
               <form method="post"
                     action="{{ as_url(routes.revisions.selected_post, 
project_name=project_name, version_name=version_name) }}">
+                {{ empty_form.hidden_tag() }}
+
                 <input type="hidden" name="revision_name" value="{{ 
revision.name }}" />
                 <button type="submit" class="btn btn-sm 
btn-outline-danger">Set this revision as current</button>
               </form>
diff --git a/atr/templates/start-selected.html 
b/atr/templates/start-selected.html
index a0e6691..cbc512d 100644
--- a/atr/templates/start-selected.html
+++ b/atr/templates/start-selected.html
@@ -14,8 +14,6 @@
         action="{{ as_url(routes.start.selected, project_name=project.name) }}"
         enctype="multipart/form-data"
         class="atr-canary py-4 px-5 border rounded">
-
-    {# Includes csrf_token and project_name #}
     {{ form.hidden_tag() }}
 
     <div class="mb-4">
diff --git a/atr/templates/upload-selected.html 
b/atr/templates/upload-selected.html
index 24766fd..e4c6f94 100644
--- a/atr/templates/upload-selected.html
+++ b/atr/templates/upload-selected.html
@@ -39,7 +39,8 @@
         enctype="multipart/form-data"
         class="atr-canary py-4 px-5"
         novalidate>
-    {{ form.csrf_token }}
+    {{ form.hidden_tag() }}
+
     <div class="mb-3 pb-3 row border-bottom">
       <label for="{{ form.file_data.id }}"
              class="col-sm-3 col-form-label text-sm-end">{{ 
form.file_data.label.text }}:</label>
@@ -83,6 +84,7 @@
             novalidate
             class="atr-canary py-4 px-5">
         {{ svn_form.hidden_tag() }}
+
         <div class="mb-3 pb-3 row border-bottom">
           <label for="{{ svn_form.svn_url.id }}"
                  class="col-sm-3 col-form-label text-sm-end">{{ 
svn_form.svn_url.label.text }}:</label>
diff --git a/atr/templates/voting-selected-revision.html 
b/atr/templates/voting-selected-revision.html
index 7a06c23..8c913f9 100644
--- a/atr/templates/voting-selected-revision.html
+++ b/atr/templates/voting-selected-revision.html
@@ -50,9 +50,9 @@
         id="vote-initiate-form"
         class="atr-canary py-4 px-5"
         action="{{ as_url(routes.voting.selected_revision, 
project_name=release.project.name, version_name=release.version, 
revision=release.revision) }}">
-    {{ form.hidden_tag() if form.hidden_tag }}
-    {{ form.release_name }}
+    {{ form.hidden_tag() }}
 
+    {{ form.release_name }}
     <div class="mb-4">
       <div class="row mb-3 pb-3 border-bottom">
         <div class="col-md-3 text-md-end fw-medium">{{ form.mailing_list.label 
}}</div>
diff --git a/atr/util.py b/atr/util.py
index eaea01e..23e37da 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -84,6 +84,10 @@ class QuartFormTyped(quart_wtf.QuartForm):
         return form
 
 
+class EmptyForm(QuartFormTyped):
+    pass
+
+
 async def archive_listing(file_path: pathlib.Path) -> list[str] | None:
     """Attempt to list contents of supported archive files."""
     if not await aiofiles.os.path.isfile(file_path):
@@ -562,6 +566,12 @@ def validate_as_type(value: Any, t: type[T]) -> T:
     return value
 
 
+async def validate_empty_form() -> None:
+    empty_form = await EmptyForm.create_form(data=await quart.request.form)
+    if not await empty_form.validate_on_submit():
+        raise base.ASFQuartException("Invalid request", 400)
+
+
 def validate_vote_duration(form: wtforms.Form, field: wtforms.IntegerField) -> 
None:
     """Checks if the value is 0 or between 72 and 144."""
     if field.data is None:


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to