You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/07/05 11:01:31 UTC
(tooling-trusted-releases) branch sbp updated: Add missing committee guards to the storage interface
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 ec3fdaab Add missing committee guards to the storage interface
ec3fdaab is described below
commit ec3fdaab3b78d2bda154efa43bb1f30938f6fd6f
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Sun Jul 5 12:00:55 2026 +0100
Add missing committee guards to the storage interface
---
atr/post/projects.py | 4 ++--
atr/storage/writers/project.py | 39 ++++++++++++++++++++++++---------------
tests/unit/test_cap.py | 24 ++++++++++++++++++++++++
3 files changed, 50 insertions(+), 17 deletions(-)
diff --git a/atr/post/projects.py b/atr/post/projects.py
index 4917e005..496594f9 100644
--- a/atr/post/projects.py
+++ b/atr/post/projects.py
@@ -60,9 +60,9 @@ async def add_project(
raise ValueError(f"Invalid committee key: {committee_key}")
async with storage.write(session) as write:
- wacm = await write.as_project_committee_member(safe.ProjectKey(str(committee_key)))
+ wacm = write.as_committee_member(str(committee_key))
try:
- await wacm.project.create(committee_key, display_name, project_key)
+ await wacm.project.create(display_name, project_key)
except storage.AccessError as e:
return await session.redirect(
get.projects.add_project, committee_key=str(committee_key), error=f"Error adding project: {e}"
diff --git a/atr/storage/writers/project.py b/atr/storage/writers/project.py
index 6627bef1..cabe5a4a 100644
--- a/atr/storage/writers/project.py
+++ b/atr/storage/writers/project.py
@@ -197,10 +197,7 @@ class CommitteeMember(ReleaseManager):
self.__committee_key = committee_key
async def category_add(self, project_key: safe.ProjectKey, new_category: str) -> bool:
- project = await self.__data.project(key=str(project_key)).get()
- if not project:
- raise storage.AccessError(f"Project '{project_key}' not found.", status=404)
- storage.ensure_project_active(project)
+ project = await self.__validate_project_in_committee(project_key)
new_category = new_category.strip()
current_categories = self.__current_categories(project)
if new_category and (new_category not in current_categories):
@@ -224,10 +221,7 @@ class CommitteeMember(ReleaseManager):
return False
async def category_remove(self, project_key: safe.ProjectKey, action_value: str) -> bool:
- project = await self.__data.project(key=str(project_key)).get()
- if not project:
- raise storage.AccessError(f"Project '{project_key}' not found.", status=404)
- storage.ensure_project_active(project)
+ project = await self.__validate_project_in_committee(project_key)
current_categories = self.__current_categories(project)
if action_value in current_categories:
if action_value in registry.FORBIDDEN_PROJECT_CATEGORIES:
@@ -246,7 +240,8 @@ class CommitteeMember(ReleaseManager):
return True
return False
- async def create(self, committee_key: safe.CommitteeKey, display_name: str, project_key: safe.ProjectKey) -> None:
+ async def create(self, display_name: str, project_key: safe.ProjectKey) -> None:
+ committee_key = safe.CommitteeKey(self.__committee_key)
try:
await _build_and_add_project(self.__data, self.__asf_uid, committee_key, display_name, project_key)
await self.__data.commit()
@@ -274,6 +269,10 @@ class CommitteeMember(ReleaseManager):
).get()
if not project:
raise storage.AccessError(f"Project '{project_key}' not found.")
+ if project.committee_key != self.__committee_key:
+ raise storage.AccessError(
+ f"Project {project_key} is not in committee {self.__committee_key}", status=403
+ )
# TODO: when a project has current or archived releases, archiving the project
# should be allowed and should cascade-archive the current releases as well -
@@ -325,6 +324,10 @@ class CommitteeMember(ReleaseManager):
).get()
if not project:
raise storage.AccessError(f"Project '{project_key}' not found.", status=404)
+ if project.committee_key != self.__committee_key:
+ raise storage.AccessError(
+ f"Project {project_key} is not in committee {self.__committee_key}", status=403
+ )
if project.committee_key:
committee_projects = await self.__data.project(
@@ -354,9 +357,7 @@ class CommitteeMember(ReleaseManager):
return None
async def language_add(self, project_key: safe.ProjectKey, new_language: str) -> bool:
- project = await self.__data.project(key=str(project_key)).get()
- if not project:
- raise storage.AccessError(f"Project '{project_key}' not found.", status=404)
+ project = await self.__validate_project_in_committee(project_key)
new_language = new_language.strip()
current_languages = self.__current_languages(project)
if new_language and (new_language not in current_languages):
@@ -378,9 +379,7 @@ class CommitteeMember(ReleaseManager):
return False
async def language_remove(self, project_key: safe.ProjectKey, action_value: str) -> bool:
- project = await self.__data.project(key=str(project_key)).get()
- if not project:
- raise storage.AccessError(f"Project '{project_key}' not found.", status=404)
+ project = await self.__validate_project_in_committee(project_key)
current_languages = self.__current_languages(project)
if action_value in current_languages:
current_languages.remove(action_value)
@@ -404,6 +403,7 @@ class CommitteeMember(ReleaseManager):
cap_question_id: int,
closes_at: datetime.datetime,
) -> sql.ApprovalRequest:
+ await self.__validate_project_in_committee(project_key)
approval = sql.ApprovalRequest(
project_key=str(project_key),
committee_key=self.__committee_key,
@@ -467,6 +467,15 @@ class CommitteeMember(ReleaseManager):
else []
)
+ async def __validate_project_in_committee(self, project_key: safe.ProjectKey) -> sql.Project:
+ project = await self.__data.project(key=str(project_key)).demand(
+ storage.AccessError(f"Project '{project_key}' not found.", status=404)
+ )
+ if project.committee_key != self.__committee_key:
+ raise storage.AccessError(f"Project {project_key} is not in committee {self.__committee_key}", status=403)
+ storage.ensure_project_active(project)
+ return project
+
# No committee gate - the caller's right to act for the committee must be
# established upstream, in the .asf.yaml feature.
diff --git a/tests/unit/test_cap.py b/tests/unit/test_cap.py
index 9fe83364..714837ba 100644
--- a/tests/unit/test_cap.py
+++ b/tests/unit/test_cap.py
@@ -78,9 +78,20 @@ class FakeQuery:
return self._row
+class FakeProjectQuery:
+ def __init__(self, row: sql.Project | None):
+ self._row = row
+
+ async def demand(self, error: Exception, log_query: bool = False) -> sql.Project:
+ if self._row is None:
+ raise error
+ return self._row
+
+
class FakeData:
def __init__(self, row: sql.ApprovalRequest | None):
self.row = row
+ self.project_row = sql.Project(key="commons-foo", committee_key="commons", status=sql.ProjectStatus.ACTIVE)
def approval_request(self, **kwargs: object) -> FakeQuery:
return FakeQuery(self.row)
@@ -102,6 +113,9 @@ class FakeData:
async def flush(self) -> None:
return None
+ def project(self, **kwargs: object) -> FakeProjectQuery:
+ return FakeProjectQuery(self.project_row)
+
async def rollback(self) -> None:
return None
@@ -299,6 +313,16 @@ async def test_request_approval_question_id_collision(monkeypatch: pytest.Monkey
await wacm.request_approval(safe.ProjectKey("commons-foo"), sql.ApprovalAction.ARCHIVE, 42, closes_at)
+@pytest.mark.asyncio
+async def test_request_approval_rejects_cross_committee_project() -> None:
+ data = FakeData(None)
+ data.project_row = sql.Project(key="tomcat-foo", committee_key="tomcat", status=sql.ProjectStatus.ACTIVE)
+ wacm = project.CommitteeMember(FakeWrite(), FakeWriteAs(), data, "commons")
+ closes_at = datetime.datetime(2026, 7, 1, tzinfo=datetime.UTC)
+ with pytest.raises(storage.AccessError, match="not in committee"):
+ await wacm.request_approval(safe.ProjectKey("tomcat-foo"), sql.ApprovalAction.ARCHIVE, 42, closes_at)
+
+
@pytest.mark.asyncio
async def test_request_approval_schedules_initial_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
data = FakeData(None)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org