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

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@2da7807a`
   
   **Type:** `new_feature`  •  **Classification:** `actionable`  •  
**Confidence:** `medium`
   **Application domain(s):** `voting`, `release_lifecycle`
   
   ### Summary
   Issue #1208 requests a new security-specific behavior for the TRUSTED vote 
mode. When a release is flagged as a security release, trusted voting should 
only allow votes through ATR (no email-based voting), with no email receipts 
sent to public mailing lists. The three modes would be: MANUAL, EMAIL, and 
TRUSTED (which behaves as hybrid/#1205 for normal releases, and as 
security-only for security releases). This supersedes #939. The current 
codebase already has the TRUSTED vote mode with ballot recording, but always 
sends email receipts and allows email thread participation alongside ATR 
ballots.
   
   ### Where this lives in the code today
   
   #### `atr/storage/writers/vote.py` — `FoundationCommitter.cast_trusted` 
(lines 65-74)
   _needs modification_
   This method always creates a MESSAGE_SEND task for the email receipt. For 
security releases, the receipt email should not be sent to a public mailing 
list.
   
   ```python
       async def cast_trusted(  # noqa: C901
           self,
           project_key: safe.ProjectKey,
           version_key: safe.VersionKey,
           choice: sql.VoteChoice,
           comment: str,
           fullname: str,
           expected_vote_seq: int | None = None,
           expected_vote_mode: sql.VoteMode | None = None,
       ) -> tuple[list[str], str]:
   ```
   
   #### `atr/storage/writers/vote.py` — `CommitteeParticipant.send_user_vote` 
(lines 229-236)
   _needs modification_
   This email-based voting path should be entirely blocked for security 
releases since they must only accept votes through ATR.
   
   ```python
       async def send_user_vote(
           self,
           release: sql.Release,
           vote: str,
           comment: str,
           fullname: str,
           is_binding: bool = False,
       ) -> tuple[list[str], str]:
   ```
   
   #### `atr/get/vote.py` — `_render_section_vote` (lines 579-600)
   _needs modification_
   For security releases, unauthenticated users should NOT see any email-based 
voting option, and the UI should indicate votes are only accepted through ATR.
   
   ```python
   async def _render_section_vote(
       page: htm.Block,
       release: sql.Release,
       session: web.Committer | None,
       user_category: UserCategory,
       archive_url: str | None,
       latest_vote_task: sql.Task | None,
   ) -> None:
       page.h2("#vote")["3. Cast your vote"]
   
       if release.committee is None:
           raise ValueError("Release has no committee")
   
       if release.effective_vote_mode == sql.VoteMode.MANUAL:
           _render_vote_manual(page)
           return
   
       vote_recipient = _vote_recipient(release, latest_vote_task)
       if user_category == UserCategory.UNAUTHENTICATED:
           _render_vote_unauthenticated(page, release, archive_url, 
vote_recipient)
       else:
           await _render_vote_authenticated(page, release, session, 
archive_url, vote_recipient, latest_vote_task)
   ```
   
   ### Where new code would go
   - `atr/models/sql.py` — after Release model fields
     A field like `is_security_release: bool` on the Release model would flag 
which releases require security vote mode. This determines whether TRUSTED mode 
behaves as hybrid or security-only.
   - `atr/get/vote.py` — after _render_trusted_vote_authenticated
     A new function like `_render_security_vote_authenticated` to render the 
security-mode-specific voting UI (no email thread references, explicit ATR-only 
messaging).
   
   ### Proposed approach
   The implementation should add an `is_security_release` boolean field to the 
Release model in `atr/models/sql.py`. The `effective_vote_mode` property (or a 
new property like `is_security_vote`) should expose whether the current vote is 
in security mode. The key behavioral changes are:
   
   1. **Vote casting** (`atr/storage/writers/vote.py`): In `cast_trusted`, when 
the release is a security release, skip creating the MESSAGE_SEND task (no 
email receipt to the public list). The ballot is still recorded in the DB. 
Return an empty `email_to` list.
   
   2. **Block email voting** (`atr/post/vote.py`): After the TRUSTED branch, 
the EMAIL branch (lines 86-98) should reject votes for security releases with 
an error message.
   
   3. **Vote initiation** (`atr/get/voting.py`, `atr/post/voting.py`): For 
security releases, only allow `frozenset({sql.VoteMode.TRUSTED})` as allowed 
vote modes. The vote email is still sent to announce the vote, but the email 
should indicate that voting is only accepted through ATR.
   
   4. **Vote resolution** (`atr/get/resolve.py`): For security releases, skip 
email thread tabulation entirely and rely solely on ATR ballot records for 
determining the outcome.
   
   5. **UI** (`atr/get/vote.py`): For security releases, render a distinct 
voting UI that makes clear votes are only accepted through ATR, with no email 
thread participation option for unauthenticated users.
   
   ### Suggested patches
   
   #### `atr/storage/writers/vote.py`
   Skip sending email receipt for security releases while still recording the 
ballot
   
   ````diff
   --- a/atr/storage/writers/vote.py
   +++ b/atr/storage/writers/vote.py
   @@ -144,6 +144,10 @@ class FoundationCommitter(GeneralPublic):
                    potency_label=potency_label,
                )
    
   +            # For security releases, do not send email receipts to the 
mailing list
   +            is_security_vote = getattr(release, 'is_security_release', 
False)
   +            # TODO: confirm the field name for security release flag on 
Release model
   +
                previous_ballot_query = (
                    sqlmodel.select(sql.BallotPaper)
                    .where(sql.BallotPaper.release_key == release.key)
   @@ -154,20 +158,23 @@ class FoundationCommitter(GeneralPublic):
                )
                previous_ballot = (await 
self.__data.execute(previous_ballot_query)).scalar_one_or_none()
    
                receipt_message_id = mail.message_id_create()
   -            task = sql.Task(
   -                status=sql.TaskStatus.QUEUED,
   -                task_type=sql.TaskType.MESSAGE_SEND,
   -                task_args=args.Send(
   -                    email_sender=email_sender,
   -                    email_to=email_to,
   -                    subject=subject,
   -                    body=body_text,
   -                    in_reply_to=start_mid,
   -                    email_cc=email_cc,
   -                    email_bcc=email_bcc,
   -                    message_id=receipt_message_id,
   -                    footer_category=mail.MailFooterCategory.USER,
   -                ).as_task_args(),
   -                asf_uid=self.__asf_uid,
   -                project_key=release.project.key,
   -                version_key=release.version,
   -            )
   +            task = None
   +            if not is_security_vote:
   +                task = sql.Task(
   +                    status=sql.TaskStatus.QUEUED,
   +                    task_type=sql.TaskType.MESSAGE_SEND,
   +                    task_args=args.Send(
   +                        email_sender=email_sender,
   +                        email_to=email_to,
   +                        subject=subject,
   +                        body=body_text,
   +                        in_reply_to=start_mid,
   +                        email_cc=email_cc,
   +                        email_bcc=email_bcc,
   +                        message_id=receipt_message_id,
   +                        footer_category=mail.MailFooterCategory.USER,
   +                    ).as_task_args(),
   +                    asf_uid=self.__asf_uid,
   +                    project_key=release.project.key,
   +                    version_key=release.version,
   +                )
                ballot = sql.BallotPaper(
                    release_key=release.key,
                    vote_seq=release.current_vote_seq,
   @@ -180,7 +187,10 @@ class FoundationCommitter(GeneralPublic):
                    revision_number_at_cast=release.latest_revision_number,
                    receipt_message_id=receipt_message_id,
                )
   -            self.__data.add_all([task, ballot])
   +            if task is not None:
   +                self.__data.add_all([task, ballot])
   +            else:
   +                self.__data.add(ballot)
                await self.__data.flush()
                await self.__data.commit()
            except Exception:
   @@ -199,7 +209,7 @@ class FoundationCommitter(GeneralPublic):
                replaced_ballot_id=previous_ballot.id if (previous_ballot is 
not None) else None,
            )
   -        return [email_to], ""
   +        return ([email_to] if not is_security_vote else []), ""
   ````
   
   #### `atr/post/vote.py`
   Block email-based voting for security releases
   
   ````diff
   --- a/atr/post/vote.py
   +++ b/atr/post/vote.py
   @@ -84,6 +84,11 @@ async def selected_post(  # noqa: C901
            await quart.flash(success_message, "success")
            return await session.redirect(get.vote.selected, 
project_key=str(project_key), version_key=str(version_key))
    
   +    # Block email voting for security releases
   +    if getattr(release, 'is_security_release', False):
   +        await quart.flash("Security releases only accept votes through 
ATR.", "error")
   +        return await session.redirect(get.vote.selected, 
project_key=str(project_key), version_key=str(version_key))
   +
        if release.current_vote_seq != cast_vote_form.vote_seq:
            await quart.flash("The vote form is stale, please refresh and try 
again.", "error")
            return await session.redirect(get.vote.selected, 
project_key=str(project_key), version_key=str(version_key))
   ````
   
   ### Open questions
   - How is a release flagged as a 'security release'? Is it a boolean field on 
the Release model, a property of the ReleasePolicy, or determined by a 
tag/label during composition?
   - Should the vote announcement email still be sent to the mailing list for 
security releases (to announce the vote is happening), or should the entire 
vote be private?
   - For security releases, should there be any form of receipt at all (e.g., a 
private confirmation to the voter's @apache.org email)?
   - Should the effective_vote_mode property be aware of security release 
status, or should a separate property (e.g., is_security_vote) control the 
behavior difference?
   - How does this interact with podling releases? Can podlings have security 
releases?
   - The issue references #1205 (hybrid mode) - is that already 
merged/implemented, or does this depend on it?
   
   ### Files examined
   - `atr/post/vote.py`
   - `atr/storage/writers/vote.py`
   - `atr/get/vote.py`
   - `atr/get/voting.py`
   - `atr/post/voting.py`
   - `atr/tabulate.py`
   - `atr/get/resolve.py`
   - `atr/post/resolve.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