This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new 0580960 Use a strict model for Quart cookie session data
0580960 is described below
commit 05809602f4fd353b19f5ac3819258a03bf22898e
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Feb 2 20:12:02 2026 +0000
Use a strict model for Quart cookie session data
---
atr/admin/__init__.py | 18 +++++-
atr/get/test.py | 25 +++++----
atr/models/__init__.py | 4 +-
atr/models/{__init__.py => session.py} | 20 ++++++-
atr/server.py | 4 +-
atr/util.py | 100 +++++++++++++++++++++++++++++++++
pyproject.toml | 3 +-
7 files changed, 153 insertions(+), 21 deletions(-)
diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index fc4bafa..5811edd 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -49,6 +49,7 @@ import atr.htm as htm
import atr.ldap as ldap
import atr.log as log
import atr.mapping as mapping
+import atr.models.session
import atr.models.sql as sql
import atr.principal as principal
import atr.storage as storage
@@ -164,7 +165,22 @@ async def browse_as_post(session: web.Committer,
browse_form: BrowseAsUserForm)
)
log.info(f"New Quart cookie (not ASFQuart session) data: {log_safe_data}")
asfquart.session.clear()
- asfquart.session.write(new_session_data)
+ # TODO: Make this safer
+ session_cookie = atr.models.session.CookieData(
+ uid=new_session_data["uid"],
+ dn=new_session_data.get("dn"),
+ fullname=new_session_data.get("fullname"),
+ email=new_session_data.get("email"),
+ isMember=new_session_data.get("isMember", False),
+ isChair=new_session_data.get("isChair", False),
+ isRoot=new_session_data.get("isRoot", False),
+ pmcs=new_session_data.get("pmcs", []),
+ projects=new_session_data.get("projects", []),
+ mfa=new_session_data.get("mfa", False),
+ roleaccount=new_session_data.get("roleaccount",
new_session_data.get("isRole", False)),
+ metadata=new_session_data.get("metadata", {}),
+ )
+ util.write_quart_session_cookie(session_cookie)
await quart.flash(
f"You are now browsing as '{new_uid}'. To return to your own account,
please log out and log back in.",
diff --git a/atr/get/test.py b/atr/get/test.py
index adf26cb..a76f623 100644
--- a/atr/get/test.py
+++ b/atr/get/test.py
@@ -16,7 +16,6 @@
# under the License.
import asfquart.base as base
-import asfquart.session
import atr.blueprints.get as get
import atr.config as config
@@ -24,9 +23,11 @@ import atr.form as form
import atr.get.root as root
import atr.get.vote as vote
import atr.htm as htm
+import atr.models.session
import atr.models.sql as sql
import atr.shared as shared
import atr.template as template
+import atr.util as util
import atr.web as web
@@ -52,18 +53,18 @@ async def test_login(session: web.Committer | None) ->
web.WerkzeugResponse:
if not config.get().ALLOW_TESTS:
raise base.ASFQuartException("Test login not enabled", errorcode=404)
- session_data = {
- "uid": "test",
- "fullname": "Test User",
- "committees": ["test"],
- "projects": ["test"],
- "isMember": False,
- "isChair": False,
- "isRole": False,
- "metadata": {},
- }
+ session_data = atr.models.session.CookieData(
+ uid="test",
+ fullname="Test User",
+ pmcs=["test"],
+ projects=["test"],
+ isMember=False,
+ isChair=False,
+ roleaccount=False,
+ metadata={},
+ )
- asfquart.session.write(session_data)
+ util.write_quart_session_cookie(session_data)
return await web.redirect(root.index)
diff --git a/atr/models/__init__.py b/atr/models/__init__.py
index daca400..3d7d9fd 100644
--- a/atr/models/__init__.py
+++ b/atr/models/__init__.py
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-from . import api, basic, distribution, helpers, results, schema, sql,
tabulate, validation
+from . import api, basic, distribution, helpers, results, schema, session,
sql, tabulate, validation
# If we use .__name__, pyright gives a warning
-__all__ = ["api", "basic", "distribution", "helpers", "results", "schema",
"sql", "tabulate", "validation"]
+__all__ = ["api", "basic", "distribution", "helpers", "results", "schema",
"session", "sql", "tabulate", "validation"]
diff --git a/atr/models/__init__.py b/atr/models/session.py
similarity index 63%
copy from atr/models/__init__.py
copy to atr/models/session.py
index daca400..213d30c 100644
--- a/atr/models/__init__.py
+++ b/atr/models/session.py
@@ -15,7 +15,21 @@
# specific language governing permissions and limitations
# under the License.
-from . import api, basic, distribution, helpers, results, schema, sql,
tabulate, validation
+from typing import Any
-# If we use .__name__, pyright gives a warning
-__all__ = ["api", "basic", "distribution", "helpers", "results", "schema",
"sql", "tabulate", "validation"]
+from . import schema
+
+
+class CookieData(schema.Strict):
+ uid: str
+ dn: str | None = None
+ fullname: str | None = None
+ email: str | None = None
+ isMember: bool = False
+ isChair: bool = False
+ isRoot: bool = False
+ pmcs: list[str] = schema.factory(list)
+ projects: list[str] = schema.factory(list)
+ mfa: bool = False
+ roleaccount: bool = False
+ metadata: dict[str, Any] = schema.factory(dict)
diff --git a/atr/server.py b/atr/server.py
index 4ad9c66..d889e61 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -435,7 +435,9 @@ def _app_setup_request_lifecycle(app: base.QuartApp) ->
None:
if created_at_str is None:
# First time seeing this session, record creation time
session.metadata["created_at"] =
datetime.datetime.now(datetime.UTC).isoformat()
- asfquart.session.write(session)
+ pmcs = util.cookie_pmcs_or_session_pmcs(session)
+ session_data = util.session_cookie_data_from_client(session, pmcs)
+ util.write_quart_session_cookie(session_data)
return
# Parse the creation timestamp and check session age
diff --git a/atr/util.py b/atr/util.py
index b2d67f1..f573995 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -49,6 +49,7 @@ import quart
import atr.config as config
import atr.ldap as ldap
import atr.log as log
+import atr.models.session
import atr.models.sql as sql
import atr.models.validation as validation
import atr.registry as registry
@@ -74,6 +75,9 @@ NPM_PACKAGE_JSON_MAX_SIZE: Final[int] = 512 * 1024
USER_TESTS_ADDRESS: Final[str] = "[email protected]"
+NoneType: Final[type[None]] = type(None)
+
+
@dataclasses.dataclass(frozen=True)
class NpmPackInfo:
name: str
@@ -257,6 +261,29 @@ async def content_list(
)
+def cookie_pmcs() -> list[str] | None:
+ pmcs = None
+ try:
+ cookie_id = asfquart.APP.app_id
+ cookie_session = quart.session.get(cookie_id, {})
+ if isinstance(cookie_session, dict):
+ pmcs = cookie_session.get("pmcs")
+ if not isinstance(pmcs, list):
+ pmcs = None
+ except Exception:
+ pmcs = None
+ return pmcs
+
+
+def cookie_pmcs_or_session_pmcs(session_data: session.ClientSession) ->
list[str]:
+ pmcs = cookie_pmcs()
+ if pmcs is None:
+ pmcs = session_data.committees
+ if not isinstance(pmcs, list) or (not (all(isinstance(item, str) for item
in pmcs))):
+ raise TypeError("Session pmcs must be a list[str]")
+ return pmcs
+
+
async def create_hard_link_clone(
source_dir: pathlib.Path,
dest_dir: pathlib.Path,
@@ -918,6 +945,75 @@ async def session_cache_write(cache_data: dict[str, dict])
-> None:
await atomic_write_file(cache_path, json.dumps(cache_data, indent=2))
+def session_cookie_data_from_client( # noqa: C901
+ session_data: session.ClientSession, pmcs: list[str]
+) -> atr.models.session.CookieData:
+ uid = session_data.uid
+ if not isinstance(uid, str):
+ raise TypeError("Session uid must be a str")
+
+ dn = session_data.dn
+ if (dn is not None) and (not isinstance(dn, str)):
+ raise TypeError("Session dn must be a str or None")
+
+ fullname = session_data.fullname
+ if (fullname is not None) and (not isinstance(fullname, str)):
+ raise TypeError("Session fullname must be a str or None")
+
+ email = session_data.email
+ # The type checker doesn't believe that session_data.email can be None
+ # But we get the data from upstream, so we can't be entirely sure about
this
+ # Therefore, just in case it can be, we use the following convoluted
approach
+ if (not isinstance(email, NoneType)) and (not isinstance(email, str)):
+ raise TypeError("Session email must be a str or None")
+
+ is_member = session_data.isMember
+ if not isinstance(is_member, bool):
+ raise TypeError("Session isMember must be a bool")
+
+ is_chair = session_data.isChair
+ if not isinstance(is_chair, bool):
+ raise TypeError("Session isChair must be a bool")
+
+ is_root = session_data.isRoot
+ if not isinstance(is_root, bool):
+ raise TypeError("Session isRoot must be a bool")
+
+ if (not isinstance(pmcs, list)) or (not (all(isinstance(item, str) for
item in pmcs))):
+ raise TypeError("Session pmcs must be a list[str]")
+
+ projects = session_data.projects
+ if (not isinstance(projects, list)) or (not (all(isinstance(item, str) for
item in projects))):
+ raise TypeError("Session projects must be a list[str]")
+
+ mfa = session_data.mfa
+ if not isinstance(mfa, bool):
+ raise TypeError("Session mfa must be a bool")
+
+ roleaccount = session_data.isRole
+ if not isinstance(roleaccount, bool):
+ raise TypeError("Session roleaccount must be a bool")
+
+ metadata = session_data.metadata
+ if not isinstance(metadata, dict):
+ raise TypeError("Session metadata must be a dict")
+
+ return atr.models.session.CookieData(
+ uid=uid,
+ dn=dn,
+ fullname=fullname,
+ email=email,
+ isMember=is_member,
+ isChair=is_chair,
+ isRoot=is_root,
+ pmcs=pmcs,
+ projects=projects,
+ mfa=mfa,
+ roleaccount=roleaccount,
+ metadata=metadata,
+ )
+
+
def static_path(*args: str) -> str:
filename = str(pathlib.PurePosixPath(*args))
return quart.url_for("static", filename=filename)
@@ -1138,6 +1234,10 @@ def version_sort_key(version: str) -> bytes:
return bytes(result)
+def write_quart_session_cookie(session_data: atr.models.session.CookieData) ->
None:
+ session.write(session_data.model_dump(mode="json"))
+
+
async def _create_hard_link_clone_checks(
source_dir: pathlib.Path,
dest_dir: pathlib.Path,
diff --git a/pyproject.toml b/pyproject.toml
index 7705d8b..3148339 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -156,8 +156,7 @@ select = [
[tool.ruff.lint.per-file-ignores]
"atr/analysis.py" = ["RUF001"]
"atr/db/__init__.py" = ["C901"]
-"atr/models/cyclonedx/__init__.py" = ["N815"]
-"atr/models/cyclonedx/spdx.py" = ["N815"]
+"atr/models/session.py" = ["N815"]
"atr/sbom/conformance.py" = ["C901"]
"atr/sbom/licenses.py" = ["C901"]
"migrations/env.py" = ["E402"]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]