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

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@2da7807a`
   
   **Type:** `new_feature`  •  **Classification:** `actionable`  •  
**Confidence:** `medium`
   **Application domain(s):** `authentication_authorization`, 
`project_committee_management`
   
   ### Summary
   The issue requests that PMC members be able to designate committers as 
'release managers' who get the same write (but not vote) permissions as PMC 
members. The final comment from @sbp clarifies 'We are not going to subset 
which committee members can release,' meaning this is purely about elevating 
committer permissions, not restricting PMC members. The SQL model already has a 
`release_managers` field on Committee (evidenced by its use in 
`_update_tooling`), but the authorization system (`atr/user.py`, 
`atr/principal.py`, storage writers) doesn't yet use it to grant elevated 
access to designated committers.
   
   ### Where this lives in the code today
   
   #### `atr/user.py` — `projects` (lines 109-131)
   _needs modification_
   This function determines which projects a user can access. It checks 
`committee_members` and `committers` but doesn't check `release_managers`. A 
release manager who is only a committer would need elevated access comparable 
to `committee_only` level.
   
   ```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)
   ```
   
   #### `atr/user.py` — `is_committee_member` (lines 97-100)
   _needs modification_
   This function checks if a user is a committee (PMC) member. Release managers 
need write permissions equivalent to PMC members but shouldn't appear as PMC 
members for voting purposes. A parallel check (e.g., `is_release_manager`) is 
needed.
   
   ```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/storage/writers/project.py` — `CommitteeMember` (lines 77-93)
   _extension point_
   The storage writer hierarchy (GeneralPublic → FoundationCommitter → 
CommitteeParticipant → CommitteeMember) is where write authorization is 
enforced. Release managers need access at CommitteeMember level for write 
operations but not for vote operations.
   
   ```python
   class CommitteeMember(CommitteeParticipant):
       def __init__(
           self,
           write: storage.Write,
           write_as: storage.WriteAsCommitteeMember,
           data: db.Session,
           committee_key: str,
       ):
           super().__init__(write, write_as, data, committee_key)
           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("Not authorized", status=403)
           self.__asf_uid = asf_uid
           self.__committee_key = committee_key
   ```
   
   #### `atr/user.py` — `is_binding_for_release` (lines 75-94)
   _currently does this_
   Binding vote logic correctly uses `is_committee_member` - release managers 
should NOT get binding votes, so this function should remain unchanged.
   
   ```python
   async def is_binding_for_release(
       committee: sql.Committee,
       asf_uid: str,
       vote_round: int | None,
       caller_data: db.Session | None = None,
   ) -> tuple[bool, str]:
       if not committee.is_podling:
           if vote_round is not None:
               raise ValueError("Non-podling votes require vote_round to be 
None")
           return is_committee_member(committee, asf_uid), 
committee.display_name
   
       if vote_round is None:
           raise ValueError("Podling votes require vote_round 1 or 2")
       if vote_round not in (1, 2):
           raise ValueError(f"Unexpected podling vote_round: {vote_round!r}")
       if vote_round == 1:
           return is_committee_member(committee, asf_uid), 
committee.display_name
       async with db.ensure_session(caller_data) as data:
           incubator = await data.committee(key="incubator").get()
       return is_committee_member(incubator, asf_uid), "Incubator"
   ```
   
   ### Where new code would go
   - `atr/user.py` — after symbol is_committer
     Add an `is_release_manager` function parallel to `is_committee_member` and 
`is_committer` that checks the `release_managers` field on Committee.
   - `atr/storage/writers/project.py` — after symbol CommitteeParticipant
     Optionally add a `ReleaseManager` writer class between 
CommitteeParticipant and CommitteeMember, or modify CommitteeMember 
authorization to also admit release managers.
   - `atr/storage/__init__.py` — new file or extension
     The storage Write class needs to be updated to recognize release managers 
as having elevated write access (equivalent to CommitteeMember) without 
granting vote permissions.
   
   ### Proposed approach
   The implementation has two main parts: (1) Using the existing 
`release_managers` field on `sql.Committee` to store designated release 
managers per committee/project, and (2) updating the authorization system to 
grant release managers write access equivalent to PMC members.
   
   For part 1, a PMC member needs to be able to designate committers as release 
managers (via a project settings form or the Start Release form, as @dave2wave 
outlined). The `release_managers` field already exists on the Committee model.
   
   For part 2, the key changes are: add an `is_release_manager()` check in 
`atr/user.py`; update `projects()` to recognize release managers at the 
committee-member level when determining write access; ensure the storage writer 
authorization (in `atr/storage/__init__.py`) admits release managers for write 
operations but NOT for vote binding. The vote logic in `is_binding_for_release` 
must remain unchanged—release managers should not get binding votes. Since I 
don't have the full `atr/storage/__init__.py` or `atr/models/sql.py` source, 
the diffs below are limited to files I can verify.
   
   ### Suggested patches
   
   #### `atr/user.py`
   Add `is_release_manager` helper function and update `projects()` to 
recognize release managers at the committee-member access level for write 
operations.
   
   ````diff
   --- a/atr/user.py
   +++ b/atr/user.py
   @@ -90,6 +90,12 @@ def is_committer(committee: sql.Committee | None, uid: 
str) -> bool:
        return any((committer_uid == uid) for committer_uid in 
committee.committers)
    
    
   +def is_release_manager(committee: sql.Committee | None, uid: str) -> bool:
   +    if committee is None:
   +        return False
   +    return any((rm_uid == uid) for rm_uid in committee.release_managers)
   +
   +
    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:
   @@ -109,7 +115,11 @@ async def projects(uid: str, committee_only: bool = 
False, super_project: bool =
    
                if committee_only:
                    if uid in p.committee.committee_members:
                        user_projects.append(p)
   +                elif uid in p.committee.release_managers:
   +                    user_projects.append(p)
                else:
                    if (uid in p.committee.committee_members) or (uid in 
p.committee.committers):
                        user_projects.append(p)
   +                elif uid in p.committee.release_managers:
   +                    user_projects.append(p)
        return user_projects
   ````
   
   ### Open questions
   - What does the `release_managers` field on `sql.Committee` look like (type, 
constraints)? I need `atr/models/sql.py` to confirm its structure.
   - How is `atr/storage/__init__.py` structured? The `Write` class and 
`WriteAsCommitteeMember` need to be updated to admit release managers, but I 
don't have that source.
   - Should release managers be designated per-committee (as current 
`release_managers` field suggests) or per-project or per-release? @dave2wave's 
comment #5 suggests both per-project defaults and per-release overrides.
   - What UI form changes are needed? The Start Release form and Project form 
need modification to allow PMC members to designate release managers.
   - How should the `release_managers` list be persisted and managed? Through a 
project settings endpoint? Through `.asf.yaml` in the future?
   
   ### Files examined
   - `atr/user.py`
   - `atr/principal.py`
   - `atr/ldap.py`
   - `atr/cache.py`
   - `atr/storage/writers/project.py`
   - `atr/storage/writers/tokens.py`
   - `atr/datasources/apache.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