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 4b6edf4  Move project deletion to a new project writer
4b6edf4 is described below

commit 4b6edf4bed39ef123d528173408423d255bff261
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Sep 11 14:32:56 2025 +0100

    Move project deletion to a new project writer
---
 atr/routes/projects.py          |  33 +++---------
 atr/storage/__init__.py         |   9 ++--
 atr/storage/writers/__init__.py |   2 +
 atr/storage/writers/project.py  | 115 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 128 insertions(+), 31 deletions(-)

diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 966a83f..553fe2d 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -33,6 +33,7 @@ import atr.forms as forms
 import atr.log as log
 import atr.models.sql as sql
 import atr.routes as routes
+import atr.storage as storage
 import atr.template as template
 import atr.user as user
 import atr.util as util
@@ -265,32 +266,12 @@ async def delete(session: routes.CommitterSession) -> 
response.Response:
     if not project_name:
         return await session.redirect(projects, error="Missing project name 
for deletion.")
 
-    # TODO: Move this to the storage interface
-    async with db.session() as data:
-        project = await data.project(
-            name=project_name, status=sql.ProjectStatus.ACTIVE, 
_releases=True, _distribution_channels=True
-        ).get()
-
-        if not project:
-            return await session.redirect(projects, error=f"Project 
'{project_name}' not found.")
-
-        # Check for ownership or admin status
-        is_owner = project.created_by == session.uid
-        is_privileged = util.is_user_viewing_as_admin(session.uid)
-
-        if not (is_owner or is_privileged):
-            return await session.redirect(
-                projects, error=f"You do not have permission to delete project 
'{project_name}'."
-            )
-
-        # Prevent deletion if there are associated releases or channels
-        if project.releases:
-            return await session.redirect(
-                projects, error=f"Cannot delete project '{project_name}' 
because it has associated releases."
-            )
-
-        await data.delete(project)
-        await data.commit()
+    async with storage.write(session.uid) as write:
+        wacm = await write.as_project_committee_member(project_name)
+        try:
+            await wacm.project.delete(project_name)
+        except storage.AccessError as e:
+            return await session.redirect(projects, error=f"Error deleting 
project: {e}")
 
     return await session.redirect(projects, success=f"Project '{project_name}' 
deleted successfully.")
 
diff --git a/atr/storage/__init__.py b/atr/storage/__init__.py
index 7cd4140..b0f08ca 100644
--- a/atr/storage/__init__.py
+++ b/atr/storage/__init__.py
@@ -138,6 +138,7 @@ class WriteAsGeneralPublic(WriteAs):
         self.announce = writers.announce.GeneralPublic(write, self, data)
         self.checks = writers.checks.GeneralPublic(write, self, data)
         self.keys = writers.keys.GeneralPublic(write, self, data)
+        self.project = writers.project.GeneralPublic(write, self, data)
         self.release = writers.release.GeneralPublic(write, self, data)
         self.sbom = writers.sbom.GeneralPublic(write, self, data)
         self.ssh = writers.ssh.GeneralPublic(write, self, data)
@@ -152,6 +153,7 @@ class WriteAsFoundationCommitter(WriteAsGeneralPublic):
         self.announce = writers.announce.FoundationCommitter(write, self, data)
         self.checks = writers.checks.FoundationCommitter(write, self, data)
         self.keys = writers.keys.FoundationCommitter(write, self, data)
+        self.project = writers.project.FoundationCommitter(write, self, data)
         self.release = writers.release.FoundationCommitter(write, self, data)
         self.sbom = writers.sbom.FoundationCommitter(write, self, data)
         self.ssh = writers.ssh.FoundationCommitter(write, self, data)
@@ -172,6 +174,7 @@ class 
WriteAsCommitteeParticipant(WriteAsFoundationCommitter):
         self.announce = writers.announce.CommitteeParticipant(write, self, 
data, committee_name)
         self.checks = writers.checks.CommitteeParticipant(write, self, data, 
committee_name)
         self.keys = writers.keys.CommitteeParticipant(write, self, data, 
committee_name)
+        self.project = writers.project.CommitteeParticipant(write, self, data, 
committee_name)
         self.release = writers.release.CommitteeParticipant(write, self, data, 
committee_name)
         self.sbom = writers.sbom.CommitteeParticipant(write, self, data, 
committee_name)
         self.ssh = writers.ssh.CommitteeParticipant(write, self, data, 
committee_name)
@@ -197,6 +200,7 @@ class WriteAsCommitteeMember(WriteAsCommitteeParticipant):
         self.checks = writers.checks.CommitteeMember(write, self, data, 
committee_name)
         self.distributions = writers.distributions.CommitteeMember(write, 
self, data, committee_name)
         self.keys = writers.keys.CommitteeMember(write, self, data, 
committee_name)
+        self.project = writers.project.CommitteeMember(write, self, data, 
committee_name)
         self.release = writers.release.CommitteeMember(write, self, data, 
committee_name)
         self.sbom = writers.sbom.CommitteeMember(write, self, data, 
committee_name)
         self.ssh = writers.ssh.CommitteeMember(write, self, data, 
committee_name)
@@ -218,13 +222,8 @@ class WriteAsFoundationAdmin(WriteAsCommitteeMember):
     def __init__(self, write: Write, data: db.Session, committee_name: str):
         self.__asf_uid = write.authorisation.asf_uid
         self.__committee_name = committee_name
-        # self.announce = writers.announce.FoundationAdmin(write, self, data, 
committee_name)
-        # self.checks = writers.checks.FoundationAdmin(write, self, data, 
committee_name)
         self.keys = writers.keys.FoundationAdmin(write, self, data, 
committee_name)
         self.release = writers.release.FoundationAdmin(write, self, data, 
committee_name)
-        # self.ssh = writers.ssh.FoundationAdmin(write, self, data, 
committee_name)
-        # self.tokens = writers.tokens.FoundationAdmin(write, self, data, 
committee_name)
-        # self.vote = writers.vote.FoundationAdmin(write, self, data, 
committee_name)
 
     @property
     def asf_uid(self) -> str:
diff --git a/atr/storage/writers/__init__.py b/atr/storage/writers/__init__.py
index 6e9daff..2a5efc6 100644
--- a/atr/storage/writers/__init__.py
+++ b/atr/storage/writers/__init__.py
@@ -19,6 +19,7 @@ import atr.storage.writers.announce as announce
 import atr.storage.writers.checks as checks
 import atr.storage.writers.distributions as distributions
 import atr.storage.writers.keys as keys
+import atr.storage.writers.project as project
 import atr.storage.writers.release as release
 import atr.storage.writers.sbom as sbom
 import atr.storage.writers.ssh as ssh
@@ -30,6 +31,7 @@ __all__ = [
     "checks",
     "distributions",
     "keys",
+    "project",
     "release",
     "sbom",
     "ssh",
diff --git a/atr/storage/writers/project.py b/atr/storage/writers/project.py
new file mode 100644
index 0000000..2ab4693
--- /dev/null
+++ b/atr/storage/writers/project.py
@@ -0,0 +1,115 @@
+# 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.
+
+# Removing this will cause circular imports
+from __future__ import annotations
+
+import atr.db as db
+import atr.models.sql as sql
+import atr.storage as storage
+import atr.util as util
+
+
+class GeneralPublic:
+    def __init__(
+        self,
+        write: storage.Write,
+        write_as: storage.WriteAsGeneralPublic,
+        data: db.Session,
+    ):
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        self.__asf_uid = write.authorisation.asf_uid
+
+
+class FoundationCommitter(GeneralPublic):
+    def __init__(self, write: storage.Write, write_as: 
storage.WriteAsFoundationCommitter, data: db.Session):
+        super().__init__(write, write_as, data)
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        asf_uid = write.authorisation.asf_uid
+        if asf_uid is None:
+            raise storage.AccessError("No ASF UID")
+        self.__asf_uid = asf_uid
+
+
+class CommitteeParticipant(FoundationCommitter):
+    def __init__(
+        self,
+        write: storage.Write,
+        write_as: storage.WriteAsCommitteeParticipant,
+        data: db.Session,
+        committee_name: str,
+    ):
+        super().__init__(write, write_as, data)
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        asf_uid = write.authorisation.asf_uid
+        if asf_uid is None:
+            raise storage.AccessError("No ASF UID")
+        self.__asf_uid = asf_uid
+        self.__committee_name = committee_name
+
+
+class CommitteeMember(CommitteeParticipant):
+    def __init__(
+        self,
+        write: storage.Write,
+        write_as: storage.WriteAsCommitteeMember,
+        data: db.Session,
+        committee_name: str,
+    ):
+        super().__init__(write, write_as, data, committee_name)
+        self.__write = write
+        self.__write_as = write_as
+        self.__data = data
+        asf_uid = write.authorisation.asf_uid
+        if asf_uid is None:
+            raise storage.AccessError("No ASF UID")
+        self.__asf_uid = asf_uid
+        self.__committee_name = committee_name
+
+    async def delete(self, project_name: str) -> None:
+        project = await self.__data.project(
+            name=project_name, status=sql.ProjectStatus.ACTIVE, 
_releases=True, _distribution_channels=True
+        ).get()
+
+        if not project:
+            raise storage.AccessError(f"Project '{project_name}' not found.")
+
+        # Check for ownership or admin status
+        # TODO: Should use FoundationCommitter for the latter check
+        is_owner = project.created_by == self.__asf_uid
+        is_privileged = util.is_user_viewing_as_admin(self.__asf_uid)
+
+        if not (is_owner or is_privileged):
+            raise storage.AccessError(f"You do not have permission to delete 
project '{project_name}'.")
+
+        # Prevent deletion if there are associated releases or channels
+        if project.releases:
+            raise storage.AccessError(f"Cannot delete project '{project_name}' 
because it has associated releases.")
+
+        await self.__data.delete(project)
+        await self.__data.commit()
+        self.__write_as.append_to_audit_log(
+            action="project.delete",
+            project_name=project_name,
+        )
+        return None


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

Reply via email to