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]