asf-tooling commented on issue #610:
URL: 
https://github.com/apache/tooling-trusted-releases/issues/610#issuecomment-4410151208

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@2da7807a`
   
   **Type:** `new_feature`  •  **Classification:** `actionable`  •  
**Confidence:** `medium`
   **Application domain(s):** `authentication_authorization`, 
`web_api_infrastructure`
   
   ### Summary
   This issue requests a new 'CommitteeReleaseManager' permission that combines 
CommitteeMember with designated release managers (who may be committers, not 
PMC members). @dave2wave clarified two variants: (1) designated individuals, 
(2) designated committees where all members/committers get RM permissions. The 
last comment by @dave2wave (2026-03-01) suggests this may be handled via #656 
and #801, introducing uncertainty about whether this issue is the right locus 
of work. No release manager concept currently exists in the codebase.
   
   ### Where this lives in the code today
   
   #### `atr/user.py` — `is_committee_member` (lines 97-100)
   _extension point_
   Current committee membership check — a parallel is_release_manager function 
would be added nearby.
   
   ```python
   def is_committee_member(committee: sql.Committee | None, uid: str) -> bool:
       if committee is None:
           return False
       return any((member_uid == uid) for member_uid in 
committee.committee_members)
   ```
   
   #### `atr/user.py` — `is_committer` (lines 103-106)
   _extension point_
   Current committer check — release manager permission would combine committee 
membership with designated committers.
   
   ```python
   def is_committer(committee: sql.Committee | None, uid: str) -> bool:
       if committee is None:
           return False
       return any((committer_uid == uid) for committer_uid in 
committee.committers)
   ```
   
   #### `atr/web.py` — `Committer.prevent_confusing_ui_display` (lines 88-97)
   _extension point_
   @dave2wave noted check_access_release_manager and 
check_access_release_manager_get methods need to be added to web.py, following 
this pattern of access control.
   
   ```python
       async def prevent_confusing_ui_display(self, project_key: str | 
safe.ProjectKey) -> None:
           if not any((str(p.key) == str(project_key)) for p in (await 
self.user_projects)):
               if self.is_admin:
                   # Admins can view all projects
                   # But we must warn them when the project is not one of their 
own
                   # TODO: This code is difficult to test locally
                   # TODO: This flash sometimes displays after deleting a 
project, which is a bug
                   await quart.flash("This is not your project, but you have 
access as an admin", "warning")
                   return
               raise base.ASFQuartException("You do not have access to this 
project", errorcode=403)
   ```
   
   #### `atr/web.py` — `Committer.prevent_confusing_ui_display_committee` 
(lines 99-110)
   _extension point_
   Existing committee access check pattern — new release manager checks would 
follow a similar structure.
   
   ```python
       async def prevent_confusing_ui_display_committee(self, committee_key: 
safe.CommitteeKey, die: bool = True) -> bool:
           if str(committee_key) not in self.committees:
               if self.is_admin:
                   # Admins can view all committees
                   # But we must warn them when the committee is not one of 
their own
                   # TODO: As above, this code is difficult to test locally
                   await quart.flash("This is not your committee, but you have 
access as an admin", "warning")
                   return True
               if die:
                   raise base.ASFQuartException("You do not have access to this 
committee", errorcode=403)
               return False
           return True
   ```
   
   #### `atr/user.py` — `projects` (lines 109-132)
   _needs modification_
   Project access logic would need to account for designated release managers 
who aren't in committee_members or committers lists.
   
   ```python
   async def projects(uid: str, committee_only: bool = False, super_project: 
bool = False) -> list[sql.Project]:
       user_projects: list[sql.Project] = []
       async with db.session() as data:
           # Must have releases, because this is used in candidate_drafts
           projects = await data.project(
               status=sql.ProjectStatus.ACTIVE, _committee=True, 
_releases=True, _super_project=super_project
           ).all()
           for p in projects:
               if p.committee is None:
                   continue
   
               # Allow access to test project in Test mode
               # This means that the Test project will show in the user 
interface for everyone
               if config.is_test_mode() and (p.committee.key == "test"):
                   user_projects.append(p)
                   continue
   
               if committee_only:
                   if uid in p.committee.committee_members:
                       user_projects.append(p)
               else:
                   if (uid in p.committee.committee_members) or (uid in 
p.committee.committers):
                       user_projects.append(p)
       return user_projects
   ```
   
   ### Where new code would go
   - `atr/models/sql.py` — new table or field on Committee/Project
     Need a data model to store designated release managers per committee — 
either a new relationship table or a JSON field on Committee.
   - `atr/user.py` — after is_committer function
     New is_release_manager() function that checks if a user is a committee 
member OR a designated release manager.
   - `atr/web.py` — after prevent_confusing_ui_display_committee method
     @dave2wave explicitly stated check_access_release_manager and 
check_access_release_manager_get methods need to be added here.
   - `atr/storage/__init__.py` — alongside existing permission checks
     The storage layer centralizes authorization; a new release manager 
permission check would be added here per the architecture.
   
   ### Proposed approach
   The implementation requires: (1) A data model change in `atr/models/sql.py` 
to store designated release managers per committee — likely a new 
`CommitteeReleaseManager` table linking committee keys to ASF UIDs of 
designated RMs who aren't already PMC members. (2) A new 
`is_release_manager(committee, uid)` function in `atr/user.py` that returns 
True if the user is a committee member OR is in the designated release managers 
list. (3) New `check_access_release_manager` and 
`check_access_release_manager_get` methods on the `Committer` class in 
`atr/web.py` that enforce the permission, raising 403 for unauthorized users 
(with admin override). (4) Applying these checks to the relevant release 
operation endpoints.
   
   However, @dave2wave's last comment references issues #656 and #801 as 
possibly subsuming this work. The exact relationship is unclear without seeing 
those issues. The diffs below represent a starting point for the permission 
infrastructure, but the actual implementation should be coordinated with 
whatever approach is chosen in #656.
   
   ### Suggested patches
   
   #### `atr/user.py`
   Add is_release_manager function combining committee membership with 
designated RM status
   
   ````diff
   --- a/atr/user.py
   +++ b/atr/user.py
   @@ -90,6 +90,25 @@ def is_committer(committee: sql.Committee | None, uid: 
str) -> bool:
        return any((committer_uid == uid) for committer_uid in 
committee.committers)
    
    
   +async def is_release_manager(committee: sql.Committee | None, uid: str) -> 
bool:
   +    """Check if the user is a release manager for the given committee.
   +
   +    A release manager is either:
   +    - A committee member (PMC member), OR
   +    - A designated release manager (committer explicitly granted RM 
permissions)
   +    """
   +    if committee is None:
   +        return False
   +    if is_committee_member(committee, uid):
   +        return True
   +    # Check designated release managers from the database
   +    async with db.session() as data:
   +        # TODO: Implement query for designated release managers
   +        # This requires the CommitteeReleaseManager model from sql.py
   +        designated = await 
data.committee_release_managers(committee_key=committee.key, uid=uid)
   +        return designated is not None
   +
   +
    async def projects(uid: str, committee_only: bool = False, super_project: 
bool = False) -> list[sql.Project]:
        user_projects: list[sql.Project] = []
        async with db.session() as data:
   ````
   
   #### `atr/web.py`
   Add check_access_release_manager and check_access_release_manager_get 
methods as specified by @dave2wave
   
   ````diff
   --- a/atr/web.py
   +++ b/atr/web.py
   @@ -78,6 +78,36 @@ class Committer:
                return False
            return True
    
   +    async def check_access_release_manager(self, committee: sql.Committee) 
-> None:
   +        """Check that the current user has release manager permissions for 
the committee.
   +
   +        Raises 403 if the user is not a committee member, not a designated 
release manager,
   +        and not an admin.
   +        """
   +        if await user.is_release_manager(committee, self.asf_uid):
   +            return
   +        if self.is_admin:
   +            await quart.flash("You have access as an admin, but are not a 
release manager", "warning")
   +            return
   +        raise base.ASFQuartException(
   +            "You do not have release manager permissions for this 
committee", errorcode=403
   +        )
   +
   +    async def check_access_release_manager_get(self, committee: 
sql.Committee) -> bool:
   +        """Check release manager access for GET routes (non-fatal).
   +
   +        Returns True if the user has release manager access, False 
otherwise.
   +        Does not raise an exception — intended for views that degrade 
gracefully.
   +        """
   +        if await user.is_release_manager(committee, self.asf_uid):
   +            return True
   +        if self.is_admin:
   +            await quart.flash("You have access as an admin, but are not a 
release manager", "warning")
   +            return True
   +        return False
   +
        async def form_data(self) -> dict[str, Any]:
            if self.__form_data is None:
                self.__form_data = await form.quart_request()
   ````
   
   ### Open questions
   - What is the status of issues #656 and #801? @dave2wave's last comment 
suggests this work may be handled there instead.
   - What data model should store designated release managers? A new SQL table 
(CommitteeReleaseManager) or a JSON field on Committee?
   - Should the 'designated committees where all PMC members and committers are 
allowed permissions' (variant 2 from @dave2wave) be a boolean flag on 
Committee, or a separate configuration?
   - Which specific endpoints should have the release manager permission 
applied? Issue references #242 skeleton but we don't have access to that.
   - How does the db.session() query for designated release managers work? The 
data access pattern (data.committee_release_managers) needs to be implemented 
in atr/db/.
   
   ### Files examined
   - `atr/blueprints/common.py`
   - `atr/user.py`
   - `atr/blueprints/api.py`
   - `atr/web.py`
   - `atr/blueprints/get.py`
   - `atr/blueprints/post.py`
   - `atr/ldap.py`
   - `atr/models/api.py`
   
   ---
   *Draft from a triage agent. A human reviewer should validate before merging 
any change. The agent did not run tests or verify diffs apply.*


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to